package org.fluentjdbc;

import org.fluentjdbc.DatabaseTable.RowMapper;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

@ParametersAreNonnullByDefault
public abstract class DatabaseSaveBuilder<T> extends DatabaseStatement {

    protected List<String> uniqueKeyFields = new ArrayList<>();
    protected List<Object> uniqueKeyValues = new ArrayList<>();

    protected List<String> fields = new ArrayList<>();
    protected List<Object> values = new ArrayList<>();

    protected final DatabaseTable table;
    protected String idField;

    @Nullable protected T idValue;

    DatabaseSaveBuilder(DatabaseTable table, String idField, @Nullable T id) {
        this.table = table;
        this.idField = idField;
        this.idValue = id;
    }

    public DatabaseSaveBuilder<T> uniqueKey(String fieldName, @Nullable Object fieldValue) {
        uniqueKeyFields.add(fieldName);
        uniqueKeyValues.add(fieldValue);
        return this;
    }

    public DatabaseSaveBuilder<T> setField(String fieldName, @Nullable Object fieldValue) {
        fields.add(fieldName);
        values.add(fieldValue);
        return this;
    }

    @Nonnull
    public DatabaseSaveResult<T> execute(Connection connection) throws SQLException {
        T idValue = this.idValue;
        if (idValue != null) {
            Boolean isSame = table.where(idField, this.idValue).singleObject(connection, new RowMapper<Boolean>() {
                @Override
                public Boolean mapRow(DatabaseRow row) throws SQLException {
                    return shouldSkipRow(row);
                }
            });
            if (isSame != null && !isSame) {
                update(connection, idValue);
                return DatabaseSaveResult.updated(idValue);
            } else if (isSame == null) {
                insert(connection);
                return DatabaseSaveResult.inserted(idValue);
            } else {
                return DatabaseSaveResult.unchanged(idValue);
            }
        } else if (hasUniqueKey()) {
            Boolean isSame = table.whereAll(uniqueKeyFields, uniqueKeyValues).singleObject(connection, new RowMapper<Boolean>() {
                @Override
                public Boolean mapRow(DatabaseRow row) throws SQLException {
                    DatabaseSaveBuilder.this.idValue = getId(row);
                    return shouldSkipRow(row);
                }
            });
            idValue = this.idValue;
            if (idValue == null) {
                idValue = insert(connection);
                return DatabaseSaveResult.inserted(idValue);
            } else if (isSame != null && !isSame) {
                update(connection, idValue);
                return DatabaseSaveResult.updated(idValue);
            } else {
                return DatabaseSaveResult.unchanged(idValue);
            }
        } else {
            idValue = insert(connection);
            return DatabaseSaveResult.inserted(idValue);
        }
    }

    private boolean shouldSkipRow(DatabaseRow row) throws SQLException {
        for (int i = 0; i < fields.size(); i++) {
            String field = fields.get(i);
            if (!Objects.equals(values.get(i), row.getObject(field))) return false;
        }
        for (int i = 0; i < uniqueKeyFields.size(); i++) {
            String field = uniqueKeyFields.get(i);
            if (!Objects.equals(uniqueKeyValues.get(i), row.getObject(field))) return false;
        }
        return true;
    }

    private boolean hasUniqueKey() {
        if (uniqueKeyFields.isEmpty()) return false;
        for (Object o : uniqueKeyValues) {
            if (o == null) return false;
        }
        return true;
    }

    @Nullable
    protected abstract T insert(Connection connection) throws SQLException;

    private T update(Connection connection, T idValue) {
        table.where("id", idValue)
                .update()
                .setFields(this.fields, this.values)
                .setFields(uniqueKeyFields, uniqueKeyValues)
                .execute(connection);
        return idValue;
    }

    @SuppressWarnings("unchecked")
    protected T getId(DatabaseRow row) throws SQLException {
        return (T) row.getObject(idField);
    }

}

@@ -778,24 +778,34 @@
 Value;%0A%0A    
+protected 
 DatabaseSave
@@ -863,24 +863,24 @@
 ble T id) %7B%0A
-
         this
@@ -1548,24 +1548,16 @@
 able
-.w
+W
 here
-(idField, 
+Id(
 this
@@ -1595,216 +1595,27 @@
 on, 
-new RowMapper%3CBoolean%3E() %7B%0A                @Override%0A                public Boolean mapRow(DatabaseRow row) throws SQLException %7B%0A                    return shouldSkipRow(row);%0A                %7D%0A            %7D
+this::shouldSkipRow
 );%0A 
@@ -2142,142 +2142,17 @@
 on, 
-new RowMapper%3CBoolean%3E() %7B%0A                @Override%0A                public Boolean mapRow(DatabaseRow row) throws SQLException %7B%0A    
+row -%3E %7B%0A
     
@@ -2222,28 +2222,24 @@
             
-    
 return shoul
@@ -2257,26 +2257,8 @@
 w);%0A
-                %7D%0A
     
@@ -2826,32 +2826,140 @@
        %7D%0A    %7D%0A%0A
+    protected DatabaseSimpleQueryBuilder tableWhereId(T p) %7B%0A        return table.where(idField, p);%0A    %7D%0A%0A
     private bool
@@ -3751,21 +3751,23 @@
 %0A%0A    pr
-ivate
+otected
  T updat
@@ -3821,21 +3821,16 @@
 able
-.w
+W
 here
-(%22id%22, 
+Id(
 idVa

Allow for user override of DatabaseSaveBuilder