/*
 * Copyright 2017 ThoughtWorks, Inc.
 *
 * 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.
 */

package com.thoughtworks.go.server.sqlmigration;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;

import org.h2.api.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Migration_86 implements Trigger {
    private static final Logger LOGGER = LoggerFactory.getLogger(Migration_86.class);

    private PreparedStatement updateStmt;
    private PreparedStatement nextModificationIdStmt;
    private Long totalCount;

    public class PMRRecord {
        private final long materialId;
        private final String pipelineName;

        public PMRRecord(long materialId, String pipelineName) {
            this.materialId = materialId;
            this.pipelineName = pipelineName;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            PMRRecord pmrRecord = (PMRRecord) o;

            if (materialId != pmrRecord.materialId) {
                return false;
            }
            if (pipelineName != null ? !pipelineName.equals(pmrRecord.pipelineName) : pmrRecord.pipelineName != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = (int) (materialId ^ (materialId >>> 32));
            result = 31 * result + (pipelineName != null ? pipelineName.hashCode() : 0);
            return result;
        }
    }

    Map<PMRRecord, Long> map = new HashMap<>();

    public void init(Connection connection, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException {
        totalCount = executeScalar(connection, "SELECT COUNT(*) FROM pipelineMaterialRevisions");

        updateStmt = connection.prepareStatement(
                " UPDATE pipelineMaterialRevisions pmr"
                        + " SET actualFromRevisionId = ? "
                        + " WHERE pmr.Id = ?");

        nextModificationIdStmt = connection.prepareStatement(
                " SELECT TOP 1 id "
                        + " FROM modifications "
                        + " WHERE id > ? AND materialId = ?"
                        + " ORDER BY id");
    }

    public void fire(Connection connection, Object[] oldRows, Object[] newRows) throws SQLException {
        LOGGER.info("migrating {} rows", totalCount);

        int count = 0;
        ResultSet resultSet = queryPipelineMaterialRevisions(connection);
        while (resultSet.next()) {
            long pmrId = resultSet.getLong("Id");
            long fromRevision = resultSet.getLong("fromRevisionId");
            long materialId = resultSet.getLong("materialId");
            String materialType = resultSet.getString("materialType");
            String pipelineName = resultSet.getString("pipelineName");

            PMRRecord pmrRecord = new PMRRecord(materialId, pipelineName);
            long lastSeenFromRevision = fromRevision;
            if ("DependencyMaterial".equals(materialType) && map.containsKey(pmrRecord)) {
                lastSeenFromRevision = nextModificationId(map.get(pmrRecord), materialId);
            }
            fireUpdateQuery(pmrId, lastSeenFromRevision);
            map.put(pmrRecord, fromRevision);

            logCount(count++);
        }
    }

    private void logCount(int count) {
        if (count % 5000 == 0) {
            LOGGER.info("    {} done", count);
        }
    }

    private Long nextModificationId(long prevModificationId, long materialId) throws SQLException {
        nextModificationIdStmt.setLong(1, prevModificationId);
        nextModificationIdStmt.setLong(2, materialId);
        return executeScalar(nextModificationIdStmt, prevModificationId);
    }

    private void fireUpdateQuery(long pmrId, long lastSeenPmrId) throws SQLException {
        updateStmt.setLong(1, lastSeenPmrId);
        updateStmt.setLong(2, pmrId);
        updateStmt.executeUpdate();
    }

    private ResultSet queryPipelineMaterialRevisions(Connection connection) throws SQLException {
        Statement statement = connection.createStatement();
        String sql = "SELECT"
                + "    pmr.Id AS Id,"
                + "    pmr.fromRevisionId AS fromRevisionId,"
                + "    pmr.materialId AS materialId,"
                + "    m.type AS materialType,"
                + "    p.name AS pipelineName"
                + " FROM"
                + "    pipelines p"
                + "    INNER JOIN pipelineMaterialRevisions pmr ON pmr.pipelineId = p.id"
                + "    INNER JOIN materials m ON m.id = pmr.materialId"
                + " ORDER BY"
                + "    p.name, m.id, pmr.id";
        return statement.executeQuery(sql);
    }

    private static Long executeScalar(Connection connection, String sql) throws SQLException {
        ResultSet rs = null;
        try {
            rs = connection.createStatement().executeQuery(sql);
            rs.next();
            return rs.getLong(1);
        } finally {
            closeQuietly(rs);
        }
    }

    private static Long executeScalar(PreparedStatement ps, Long defaultValue) throws SQLException {
        ResultSet rs = null;
        try {
            rs = ps.executeQuery();
            if (rs.next()) {
                return rs.getLong(1);
            }
            return defaultValue;
        } finally {
            closeQuietly(rs);
        }
    }

    public static void closeQuietly(ResultSet rs) {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            // ignore
        }
    }

    public void close() {
    }

    public void remove() {
    }
}
