package Common.Database;
import Common.CommonConstants;
import Common.Database.Objects.DBObject;
import Common.Database.Objects.iDBObject;
import Common.Database.Tables.DBTable;
import Common.Database.Tables.DataSet;
import Common.Passes.PassCode_;
import Common.Utils.TextLog;
import Common.Utils.Utils_;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.util.LinkedHashMap;
import java.util.Vector;
//самый общий интерфейс базы данных, независимо от реализации.
public abstract class Database {
    //------------------------------
    protected LinkedHashMap<Class<? extends DBObject>, DBTable> tables = new LinkedHashMap<>(); //таблицы
    protected File file = null;
    public Database(File file_in) {
        file = file_in;
    }
    public <T extends DBObject> DBTable<?, T> getTable(Class<T> tableClass) {
        return tables.get(tableClass);
    }
    public File getFile() {
        return file;
    }
    public void setFile(File file_in) {
        file = file_in;
    }
    //<editor-fold desc="Общая часть">
    public void Connect() throws Exception {
        // UI.Print(DebugPrintLevel.Database, "соединение с базой данных " + file.getAbsolutePath());
        connect();
    }
    public void Activate() throws Exception {
        Connect();
        CreateAllTables();
        prepareTablesStatements();
        Synchronize();
    }
    public void prepareTablesStatements() throws Exception {
    }
    public void Disconnect() throws Exception {
        //  UI.Print(DebugPrintLevel.Database, "закрытие соединения с базой данных " + file.getAbsolutePath());
        disconnect();
    }
    public void BeginTransaction() throws Exception {
        //   UI.Print(DebugPrintLevel.Database, "BEGIN TRANSACTION:");
        beginTransaction();
    }
    public void Commit() throws Exception {
        //  UI.Print(DebugPrintLevel.Database, "COMMIT");
        commit();
    }
    public void CreateAllTables() throws Exception {
        BeginTransaction();
        initAllTables(); // все добавления новых таблиц - туть.
        for (DBTable table : tables.values()) {
            CreateTable(table);
            table.setDb(this);
        }
        Commit();
    }
    //схема как с проходами. в методе initAllTables добавляем все таблицы через уникальные конструкторы
    public void addTable(DBTable table) {
        tables.put(table.d, table);
    }
    public void Synchronize() throws Exception {
        BeginTransaction();
        for (DBTable table : tables.values())
            LoadAll(table);
        Init(); //перегружаемая часть синхронизации.
        Commit();
    }
    public void LoadAllTablesWithoutInit() throws Exception {
        BeginTransaction();
        for (DBTable table : tables.values())
            LoadAll(table);
        Commit();
    }
    public <K, D extends DBObject> void LoadAll(DBTable<K, D> table) throws Exception {
        table.Data.clear();
        loadAll(table);
    }
    public DBObject Insert(DBObject o) throws Exception {
        DBTable table = tables.get(o.getClass());
        insert(table, o);
        table.Data.put(o.getPK(), o);
        return o;
    }
    //метод для вставки в серверную базу.
    public DBObject InsertS(DBObject object) throws Exception {
        DBTable table = tables.get(object.getClass());
        if (!(object instanceof iDBObject) && table.Data.containsKey(object.getPK()))
            throw new RepositoryRefuseException("Таблица " + Utils_.Brackets(table.getName()) + " уже содержит объект с ключом " + Utils_.Brackets(object.getPK().toString()));
        insert(table, object);
        table.Data.put(object.getPK(), object);
        return object;
    }
    // не работает с автоинкрементом.
    public DBObject UpdateWithCheck(DBObject to_update) throws Exception {
        DBTable table = tables.get(to_update.getClass());
        if (table.Data.containsKey(to_update.getPK())) {
            DBObject o = (DBObject) table.Data.get(to_update.getPK());
            o.SynchronizeFields(to_update);
            update(table, o);
            return o;
        } else {
            insert(table, to_update);
            table.Data.put(to_update.getPK(), to_update);
            return to_update;
        }
    }
    public DBObject DeleteWithCheck(DBObject to_delete) throws Exception {
        DBTable table = tables.get(to_delete.getClass());
        if (table.Data.containsKey(to_delete.getPK())) {
            DBObject o = (DBObject) table.Data.get(to_delete.getPK());
            delete(table, o);
            table.Data.remove(o.getPK());
            return o;
        } else
            throw new RepositoryRefuseException("Таблица " + Utils_.Brackets(table.getName()) + " не содержит объект с ключом " + Utils_.Brackets(to_delete.getPK().toString()));
    }
    public DBObject DeleteByPK(Class object_class, Object key) throws Exception {
        DBTable table = tables.get(object_class);
        if (table.Data.containsKey(key)) {
            DBObject o = (DBObject) table.Data.get(key);
            delete(table, o);
            table.Data.remove(key);
            return o;
        } else
            throw new RepositoryRefuseException("Таблица " + Utils_.Brackets(table.getName()) + " не содержит объект с ключом " + Utils_.Brackets(key.toString()));
    }
    // не работает с автоинкрементом.
    public DBObject getObjectCopyByPK(Class table_class, Object pk) throws Exception {
        DBTable table = tables.get(table_class);
        Object res_ = table_class.newInstance();
        DBObject res = (DBObject) res_;
        if (table.Data.containsKey(pk)) {
            DBObject original = (DBObject) table.Data.get(pk);
            res.SynchronizeFields(original);
        } else {
            table_class.getField(table.getPKName()).set(res, pk); //инче просто синхроним пк. и вставляем
            insert(table, res);
        }
        return res;
    }
    public boolean checkObjectExistense(Class table_class, Object pk) throws Exception {
        DBTable table = tables.get(table_class);
        return table.containsKey(pk);
    }
    public Vector<Object> getObjectsCopies(Class table_class, Vector<Object> keys) {
        Vector<Object> res = new Vector<>();
        DBTable table = tables.get(table_class);
        for (Object key : keys)
            if (table.containsKey(key))
                res.add(table.Data.get(key));
        return res;
    }
    public void Delete(DBObject o) throws Exception {
        DBTable table = tables.get(o.getClass());
        delete(table, o);
        table.Data.remove(o.getPK());
    }
    public void DeleteAll(Class<? extends DBObject> c) throws Exception {
        DBTable table = tables.get(c);
        deleteAll(table);
        table.Data.clear();
    }
    //не вполне обычный случай. подразумевается что поле объекта изменено
    //этот же метод закрепляет изменение в бд
    //в дальнейшем возможно сделать новое значение поля 2 параметром метода?
    public void Update(DBObject o) throws Exception {
        DBTable table = tables.get(o.getClass());
        update(table, o);
    }
    public void ResetAI(Class<? extends DBObject> c) throws Exception {
        resetAI(tables.get(c));
    }
    //</editor-fold>
    //<editor-fold desc="работа с внешними ключами">
    public <O extends DBObject, F extends DBObject> Vector<F> getVectorByFK(O owner, Class<F> fk_class) {
        Vector<F> res = new Vector<>();
        try {
            for (Object o : tables.get(fk_class).Data.values()) {
                if (fk_class.getField(owner.getFKName()).get(o).equals(owner.getPK())) res.add((F) o);
            }
        } catch (Exception e) {
            Utils_.MainLog.PrintException(e);
        }
        return res;
    }
    public <F extends DBObject> void DeleteByFK(DBObject master, Class<F> fk_class) throws Exception {
        Vector<F> to_delete = getVectorByFK(master, fk_class);
        BeginTransaction();
        for (DBObject object : to_delete)
            Delete(object);
        Commit();
    }
    //-----------
    public <F extends DBObject> void DropByFK(DBObject master, Class<F> fk_class) throws Exception {
        Vector<F> to_drop = getVectorByFK(master, fk_class);
        BeginTransaction();
        for (DBObject object : to_drop) {
            fk_class.getField(master.getFKName()).set(object, master.getEmptyFK());
            Update(object);
        }
        Commit();
    }
    public <K, O extends DBObject, F extends DBObject> LinkedHashMap<K, F> getMapByFK(O owner, Class<F> fk_class, Class<K> key_class) {
        LinkedHashMap<K, F> res = new LinkedHashMap<>();
        try {
            for (Object o : tables.get(fk_class).Data.values()) {
                F f = (F) o;
                if (fk_class.getField(owner.getFKName()).get(f).equals(owner.getPK())) res.put((K) f.getPK(), f);
            }
        } catch (Exception e) {
            Utils_.MainLog.PrintException(e);
        }
        return res;
    }
    public <O extends DBObject, F extends iDBObject> LinkedHashMap<Integer, F> getMapByFKi(O owner, Class<F> fk_class) {
        return getMapByFK(owner, fk_class, java.lang.Integer.class);
    }
    public <O extends iDBObject, F extends iDBObject> Vector<Integer> getFKi(
            Integer owner_pk, Class<O> owner_class, Class<F> fk_class) {
        String fk_name = owner_class.getSimpleName().toLowerCase() + "_id";
        Vector<Integer> res = new Vector<>();
        try {
            for (Object o : tables.get(fk_class).Data.values()) {
                F f = (F) o;
                if (fk_class.getField(fk_name).get(f).equals(owner_pk)) res.add((Integer) f.getPK());
            }
        } catch (Exception e) {
            Utils_.MainLog.PrintException(e);
        }
        return res;
    }
    //-
    public <O extends DBObject, F extends DBObject> Vector<String> getVectorStringByFK(O owner, Class<F> fk_class) {
        Vector<String> res = new Vector<>();
        try {
            for (Object o : tables.get(fk_class).Data.values()) {
                if (fk_class.getField(owner.getFKName()).get(o).equals(owner.getPK())) res.add(o.toString());
            }
        } catch (Exception e) {
            Utils_.MainLog.PrintException(e);
        }
        return res;
    }
    //безопасное получение объекта по первичному ключу.
    public <O extends DBObject> O getByPK(Class<O> class_in, Object pk, Object undefined_pk) {
        return ((!pk.equals(undefined_pk)) && tables.get(class_in).Data.containsKey(pk)) ?
                (O) (tables.get(class_in).Data.get(pk)) : null;
    }
    public <O extends iDBObject> O getById(Class<O> class_in, int pk) {
        return getByPK(class_in, pk, CommonConstants.Nan);
    }
    public <G, O extends DBObject, F extends DBObject> LinkedHashMap<G, F> getByFKAndGroupBy(O owner, Class<F> fk_class, String group_field, Class<G> group_class) {
        LinkedHashMap<G, F> res = new LinkedHashMap<>();
        try {
            for (Object o : tables.get(fk_class).Data.values()) {
                F f = (F) o;
                if (fk_class.getField(owner.getFKName()).get(f).equals(owner.getPK()))
                    res.put((G) (fk_class.getField(group_field).get(f)), f);
            }
        } catch (Exception e) {
            Utils_.MainLog.PrintException(e);
        }
        return res;
    }
    //</editor-fold>
    //<editor-fold desc="Перегружаемая часть">
    //-
    protected abstract void connect() throws Exception;
    protected abstract void disconnect() throws Exception;
    //-
    protected abstract void beginTransaction() throws Exception;
    protected abstract void commit() throws Exception;
    //-
    protected abstract boolean TableExists(DBTable table) throws Exception;
    protected abstract void CreateTable(DBTable table) throws Exception;
    protected abstract <K, D extends DBObject> void loadAll(DBTable<K, D> table) throws Exception;
    protected abstract void initAllTables() throws Exception;
    //перегружаемая часть синхронизации
    public void Init() throws Exception {
    }
    protected abstract void insert(DBTable table, DBObject o) throws Exception;
    protected abstract void delete(DBTable table, DBObject o) throws Exception;
    protected abstract void deleteAll(DBTable table) throws Exception;
    protected abstract void update(DBTable table, DBObject o) throws Exception;
    protected abstract void resetAI(DBTable table) throws Exception;
    //-
    public void SaveLastSelections() {
        for (DataSet dataSet : tables.values()) {
            if (dataSet.getUI() != null)
                dataSet.getUI().SaveLastCurrent();
        }
    }
    public void RestoreLastSelections() {
        for (DataSet dataSet : tables.values()) {
            if (dataSet.getUI() != null)
                dataSet.getUI().RestoreLastCurrent();
        }
    }
    //---
    public boolean CheckCurrent(TextLog log, Class... tablesClasses) {
        for (Class tableClass : tablesClasses) {
            DBTable table = getTable(tableClass);
            if (table.getUI() != null)
                table.getUI().CheckCurrent(log);
        }
        return log.isEmpty();
    }
    public boolean canModifyCurrent(TextLog log, Class... tablesClasses) {
        for (Class tableClass : tablesClasses) {
            DBTable table = getTable(tableClass);
            if (table.getUI() != null)
                table.getUI().canModifyCurrent(log);
        }
        return log.isEmpty();
    }
    public abstract PassCode_ getSynchronizePassCode(); //если бд есть на сервере.
    //--
    public void ReplaceFileAndReconnect(File file_in) throws Exception {
        Disconnect();
        Utils_.forceDeleteWithCheck(getFile());
        FileUtils.moveFile(file_in, getFile());
        Connect();
        prepareTablesStatements();
        Synchronize();
    }
    public void DeleteDependencies(DBObject object) throws Exception {
        DBTable object_table = getTable(object.getClass());
        for (Class dep : object_table.getFKDependencies().keySet()) {
            switch (object_table.getFKDependencies().get(dep).data) {
                case NONE:
                    break;
                case DROP:
                    DropByFK(object, dep);
                    break;
                case DELETE:
                    DeleteByFK(object, dep);
                    break;
            }
        }
    }
    public void DropUI() {
        SaveLastSelections();
    }
    public void ResetUI() {
        RestoreLastSelections();
    }
}
