package Common.Database.SQLITE;
import Common.Database.Database;
import Common.Database.Objects.DBObject;
import Common.Database.Tables.DBTable;
import Common.Database.Tables.DBTableColumn;
import Common.Passes.PassException;
import Common.Utils.Pair;
import Common.Utils.Utils_;
import Common.Visual.UI;

import java.io.File;
import java.sql.*;
import java.util.LinkedHashMap;
import java.util.Vector;

import static Common.Utils.Utils_.requireNonNullElse;
public abstract class SQLiteDatabase extends Database {
    public LinkedHashMap<Class<? extends DBObject>, PreparedStatement> insertStatements = new LinkedHashMap<>();
    public LinkedHashMap<Class<? extends DBObject>, PreparedStatement> updateStatements = new LinkedHashMap<>();
    public LinkedHashMap<Class<? extends DBObject>, PreparedStatement> deleteStatements = new LinkedHashMap<>();
    protected Connection conn = null;
    protected Statement statement = null;
    protected ResultSet resSet = null;
    //->>
    public SQLiteDatabase(File file_in) {
        super(file_in);
    }
    @Override
    protected void connect() throws Exception {
        Class.forName("org.sqlite.JDBC");
        for (int i = 0; i < 5; ++i) {
            try {
                conn = DriverManager.getConnection("jdbc:sqlite:" + file.getAbsolutePath());
                break;
            } catch (Exception ex) {
                ex.printStackTrace();
                conn = null;
                System.gc();
                Utils_.sleep(2000);
            }
        }
        if (conn == null)
            throw new PassException("Внутренняя ошибка sqlite. Не удалось установить соединение за 5 попыток.");
        statement = conn.createStatement();
    }
    @Override
    protected void disconnect() throws Exception {
        if (conn != null) {
            conn.close();
            conn = null;
        }
        if (statement != null) {
            statement.close();
            statement = null;
        }
        if (resSet != null) {
            resSet.close();
            resSet = null;
        }
        for (PreparedStatement preparedStatement : insertStatements.values()) {
            if (preparedStatement != null)
                preparedStatement.close();
        }
        for (PreparedStatement preparedStatement : updateStatements.values()) {
            if (preparedStatement != null)
                preparedStatement.close();
        }
        for (PreparedStatement preparedStatement : deleteStatements.values()) {
            if (preparedStatement != null)
                preparedStatement.close();
        }
        insertStatements.clear();
        updateStatements.clear();
        deleteStatements.clear();
        //-->>
        System.gc();
        //->>
    }
    @Override
    protected void beginTransaction() throws Exception {
        conn.setAutoCommit(false);
    }
    @Override
    protected void commit() throws Exception {
        conn.commit();
        conn.setAutoCommit(true);
    }
    @Override
    protected boolean TableExists(DBTable table) throws Exception {
        int count = 0;
        resSet = statement.executeQuery("SELECT count(*) FROM 'sqlite_master' WHERE type=\"table\" AND name=" + table.QName());
        if (resSet.next())
            count = resSet.getInt(1);
        return count > 0;
    }
    @Override
    public void CreateTable(DBTable table) throws Exception {
        if (table.columns.size() > 0) {
            if (TableExists(table)) {
                Vector<String> existing_columns = new Vector<>();
                Vector<String> columns_to_create = new Vector<>();
                //предполагаем что первичный ключ и атрибуты не изменятся.
                //и что не будет столбов с одинаковыми именами но разными типами.
                resSet = statement.executeQuery("pragma table_info(" + table.QName() + ")");
                while (resSet.next())
                    existing_columns.add(resSet.getString(2));
                //-
                for (String target_column : table.columns.keySet()) {
                    if (!existing_columns.contains(target_column))
                        columns_to_create.add(target_column);
                }
                for (String cn : columns_to_create)
                    statement.execute("ALTER TABLE " + table.QName() + " ADD COLUMN " + table.columns.get(cn));
            } else {
                String cmd = "CREATE TABLE if not exists " + table.QName() + " ";
                Vector<String> columns_names = new Vector<>();
                for (DBTableColumn column : table.columns.values())
                    columns_names.add(column.toString());
                cmd += Utils_.RBrackets(String.join(",", columns_names));
                statement.execute(cmd);
            }
        }
    }
    @Override
    public void prepareTablesStatements() throws Exception {
        for (DBTable table : tables.values()) {
            //---
            Vector<String> column_names = new Vector<>();
            Vector<String> column_values = new Vector<>();
            for (DBTableColumn column : table.columns.values()) {
                if (!column.AutoIncrement) {
                    column_names.add(column.QName());
                    column_values.add("?");
                }
            }
            insertStatements.put(table.d, conn.prepareStatement(
                    "INSERT OR REPLACE INTO " + table.QName() + " " +
                            Utils_.RBrackets(String.join(",", column_names)) + " " + "VALUES " +
                            Utils_.RBrackets(String.join(",", column_values))));
            //------------------------------------------------------------------------------->>
            Vector<String> new_values = new Vector();
            for (DBTableColumn column : table.columns.values()) {
                if (!column.AutoIncrement) {
                    new_values.add(column.QName() + "=?");
                }
            }
            updateStatements.put(table.d, conn.prepareStatement(
                    "UPDATE " + table.QName() + " " +
                            "SET " + String.join(",", new_values) + " " +
                            "WHERE (" + table.PK.QName() + "=?)"
            ));
            //---------------------------------------------------------------------------------->>>
            deleteStatements.put(table.d, conn.prepareStatement("DELETE FROM " + table.QName() + " WHERE " + table.PK.QName() + " = ?"));
        }
    }
    @Override
    protected void delete(DBTable table, DBObject o) throws Exception {
        PreparedStatement ps = deleteStatements.get(table.d);
        ps.setObject(1, o.getPK());
        ps.executeUpdate();
    }
    protected <K, D extends DBObject> Pair<K, D> readRecord(DBTable<K, D> table) throws Exception {
        D o = table.d.newInstance();
        for (DBTableColumn column : table.columns.values()) {
            Object field_value = null;
            switch (column.type) {
                case DOUBLE:
                    field_value = requireNonNullElse(resSet.getDouble(column.Name), 0);
                    break;
                case UNDEFINED:
                    break;
                case INT:
                    field_value = requireNonNullElse(resSet.getInt(column.Name), 0);
                    break;
                case LONG:
                    field_value = requireNonNullElse(resSet.getLong(column.Name), 0);
                    break;
                case STRING:
                    if (table.d.getField(column.Name).getType().isEnum()) {
                        Class enum_class = Class.forName(table.d.getField(column.Name).getType().getName());
                        String string = resSet.getString(column.Name);
                        Object[] enum_constants = enum_class.getEnumConstants();
                        if (string != null) {
                            try {
                                field_value = Enum.valueOf(enum_class, string);
                            } catch (Exception ignore) {
                                field_value = enum_constants[0];
                            }
                        } else field_value = enum_constants[0];
                    } else
                        field_value = requireNonNullElse(resSet.getString(column.Name), "");
                    break;
            }
            if (field_value != null) {
                table.d.getField(column.Name).set(o, field_value);
            } else
                throw new PassException("Ошибка при загрузке поля " + Utils_.Brackets(column.Name) + " класса " + Utils_.Brackets(table.d.getSimpleName()));
        }
        return new Pair<>((K) o.getPK(), o);
    }
    @Override
    protected <K, D extends DBObject> void loadAll(DBTable<K, D> table) throws Exception {
        resSet = statement.executeQuery("SELECT * FROM " + table.QName());
        while (resSet.next()) {
            Pair<K, D> record = readRecord(table);
            table.Data.put(record.getKey(), record.getValue());
        }
    }
    @Override
    protected void insert(DBTable table, DBObject o) throws Exception {
        PreparedStatement ps = insertStatements.get(table.d);
        if (ps == null)
            UI.Info("INSERT NULL");
        int i = 1;
        for (DBTableColumn column : table.columns.values()) {
            if (!column.AutoIncrement) {
                ps.setObject(i, o.getClass().getField(column.Name).get(o));
                ++i;
            }
        }
        ps.execute();
        //-->
        for (DBTableColumn column : table.columns.values()) {
            if (column.AutoIncrement) {
                resSet = statement.executeQuery("SELECT MAX(" + column.QName() + ") AS LAST FROM " + table.QName());
                String maxId = resSet.getString("LAST");
                int intMaxId = Integer.parseInt(maxId);
                o.getClass().getField(column.Name).set(o, intMaxId);
                break;
            }
        }
    }
    @Override
    protected void update(DBTable table, DBObject o) throws Exception {
        PreparedStatement ps = updateStatements.get(table.d);
        if (ps == null)
            UI.Info("UPDATE NULL");
        int i = 1;
        for (DBTableColumn column : table.columns.values()) {
            if (!column.AutoIncrement) {
                ps.setObject(i, o.getClass().getField(column.Name).get(o));
                ++i;
            }
        }
        ps.setObject(i, o.getPK());
        ps.executeUpdate();
    }
    @Override
    protected void deleteAll(DBTable table) throws Exception {
        statement.executeUpdate("DELETE FROM " + table.QName());
    }
    @Override
    protected void resetAI(DBTable table) throws Exception {
        statement.executeUpdate("UPDATE SQLITE_SEQUENCE SET SEQ = 0 WHERE NAME =" + table.QName());
    }
    //--
    //https://stackoverflow.com/questions/8558099/sqlite-query-with-byte-where-clause
}