/*
 * Copyright (C) 2017-2020 HERE Europe B.V.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * License-Filename: LICENSE
 */

package com.here.xyz.psql;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.here.xyz.models.geojson.implementation.Feature;
import com.here.xyz.models.geojson.implementation.FeatureCollection;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.WKBWriter;
import org.postgresql.util.PGobject;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class DatabaseTransactionalWriter extends  DatabaseWriter{

    public static FeatureCollection insertFeatures(String schema, String table, String streamId, FeatureCollection collection,
                                                   List<Feature> inserts, Connection connection)
            throws SQLException, JsonProcessingException {

        boolean batchInsert = false;
        boolean batchInsertWithoutGeometry = false;

        final PreparedStatement insertStmt = createInsertStatement(connection,schema,table);
        final PreparedStatement insertWithoutGeometryStmt = createInsertWithoutGeometryStatement(connection,schema,table);

        insertStmt.setQueryTimeout(TIMEOUT);
        insertWithoutGeometryStmt.setQueryTimeout(TIMEOUT);

        for (int i = 0; i < inserts.size(); i++) {
            final Feature feature = inserts.get(i);

            final PGobject jsonbObject= featureToPGobject(feature);

            if (feature.getGeometry() == null) {
                insertWithoutGeometryStmt.setObject(1, jsonbObject);
                insertWithoutGeometryStmt.addBatch();
                batchInsertWithoutGeometry = true;
            } else {
                insertStmt.setObject(1, jsonbObject);

                final WKBWriter wkbWriter = new WKBWriter(3);
                Geometry jtsGeometry = feature.getGeometry().getJTSGeometry();
                //Avoid NAN values
                assure3d(jtsGeometry.getCoordinates());
                insertStmt.setBytes(2, wkbWriter.write(jtsGeometry));

                insertStmt.addBatch();
                batchInsert = true;
            }
            collection.getFeatures().add(feature);
        }

        if (batchInsert) {
            insertStmt.executeBatch();
        }
        if (batchInsertWithoutGeometry) {
            insertWithoutGeometryStmt.executeBatch();
        }

        return collection;
    }

    public static FeatureCollection updateFeatures(String schema, String table, String streamId, FeatureCollection collection,
                                                   List<FeatureCollection.ModificationFailure> fails, List<Feature> updates,
                                                   Connection connection, boolean handleUUID)
            throws SQLException, JsonProcessingException {

        final PreparedStatement updateStmt = createUpdateStatement(connection, schema, table, handleUUID);
        final PreparedStatement updateWithoutGeometryStmt = createUpdateWithoutGeometryStatement(connection,schema,table,handleUUID);

        updateStmt.setQueryTimeout(TIMEOUT);
        updateWithoutGeometryStmt.setQueryTimeout(TIMEOUT);

        List<String> updateIdList = new ArrayList<>();
        List<String> updateWithoutGeometryIdList = new ArrayList<>();

        int[] batchUpdateResult = null;
        int[] batchUpdateWithoutGeometryResult = null;

        for (int i = 0; i < updates.size(); i++) {
            final Feature feature = updates.get(i);
            final String puuid = feature.getProperties().getXyzNamespace().getPuuid();

            if (feature.getId() == null) {
                throw new NullPointerException("id");
            }

            final PGobject jsonbObject= featureToPGobject(feature);

            if (feature.getGeometry() == null) {
                updateWithoutGeometryStmt.setObject(1, jsonbObject);
                updateWithoutGeometryStmt.setString(2, feature.getId());
                if(handleUUID)
                    updateWithoutGeometryStmt.setString(3, puuid);
                updateWithoutGeometryStmt.addBatch();

                updateWithoutGeometryIdList.add(feature.getId());
            } else {
                updateStmt.setObject(1, jsonbObject);

                final WKBWriter wkbWriter = new WKBWriter(3);
                Geometry jtsGeometry = feature.getGeometry().getJTSGeometry();
                //Avoid NAN values
                assure3d(jtsGeometry.getCoordinates());
                updateStmt.setBytes(2, wkbWriter.write(jtsGeometry));
                updateStmt.setString(3, feature.getId());

                if(handleUUID) {
                    updateStmt.setString(4, puuid);
                }
                updateStmt.addBatch();

                updateIdList.add(feature.getId());
            }
            collection.getFeatures().add(feature);
        }

        if (updateIdList.size() > 0) {
            batchUpdateResult = updateStmt.executeBatch();
            fillFailList(batchUpdateResult, fails, updateIdList, handleUUID);
        }
        if (updateWithoutGeometryIdList.size() > 0) {
            batchUpdateWithoutGeometryResult = updateWithoutGeometryStmt.executeBatch();
            fillFailList(batchUpdateWithoutGeometryResult, fails, updateWithoutGeometryIdList, handleUUID);
        }

        if(fails.size() > 0)
            throw new SQLException(UPDATE_ERROR_GENERAL);

        return collection;
    }

    protected static void deleteFeatures(String schema, String table, String streamId,
                                         List<FeatureCollection.ModificationFailure> fails, Map<String, String> deletes,
                                         Connection connection, boolean handleUUID)
            throws SQLException {

        final PreparedStatement batchDeleteStmt = deleteStmtSQLStatement(connection,schema,table,handleUUID);
        final PreparedStatement batchDeleteStmtWithoutUUID = deleteStmtSQLStatement(connection,schema,table,false);

        batchDeleteStmt.setQueryTimeout(TIMEOUT);
        batchDeleteStmtWithoutUUID.setQueryTimeout(TIMEOUT);

        Set<String> idsToDelete = deletes.keySet();

        List<String> deleteIdList = new ArrayList<>();
        List<String> deleteIdListWithoutUUID = new ArrayList<>();

        for (String deleteId : idsToDelete) {
            final String puuid = deletes.get(deleteId);

            if(handleUUID && puuid == null){
                batchDeleteStmtWithoutUUID.setString(1, deleteId);
                batchDeleteStmtWithoutUUID.addBatch();
                deleteIdListWithoutUUID.add(deleteId);
            }
            else {
                batchDeleteStmt.setString(1, deleteId);
                if(handleUUID) {
                    batchDeleteStmt.setString(2, puuid);
                }
                deleteIdList.add(deleteId);
                batchDeleteStmt.addBatch();
            }
        }

        int[] batchDeleteStmtResult = batchDeleteStmt.executeBatch();
        int[] batchDeleteStmtWithoutUUIDResult = batchDeleteStmtWithoutUUID.executeBatch();

        fillFailList(batchDeleteStmtResult, fails, deleteIdList, handleUUID);
        fillFailList(batchDeleteStmtWithoutUUIDResult, fails, deleteIdListWithoutUUID, handleUUID);

        if(fails.size() > 0)
            throw new SQLException(DELETE_ERROR_GENERAL);
    }

    private static void fillFailList(int[] batchResult, List<FeatureCollection.ModificationFailure> fails,  List<String> idList, boolean handleUUID){
        for (int i= 0; i < batchResult.length; i++) {
            if(batchResult[i] == 0 ) {
                fails.add(new FeatureCollection.ModificationFailure().withId(idList.get(i)).withMessage(
                    (handleUUID ? UPDATE_ERROR_UUID : UPDATE_ERROR_NOT_EXISTS)));
            }
        }
    }
}