import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class BatchUpdate {

    public static void main(String[] args) {
        MiniDb db = new MiniDb();

        System.out.println("== Mode satu-per-satu: 4 kali executeUpdate ==");
        MiniStatement st = db.createStatement();
        st.executeUpdate("INSERT INTO pesan VALUES (1, 'Halo Budi')");
        st.executeUpdate("INSERT INTO pesan VALUES (2, 'Stok tersedia')");
        st.executeUpdate("INSERT INTO pesan VALUES (3, 'Pesanan dikirim')");
        st.executeUpdate("INSERT INTO pesan VALUES (4, 'Terima kasih')");
        System.out.println("roundtrip : " + db.getRoundtrip());
        System.out.println("baris     : " + db.getJumlahBaris());

        db.reset();
        System.out.println();
        System.out.println("== Mode batch: addBatch + executeBatch ==");
        st = db.createStatement();
        st.addBatch("INSERT INTO pesan VALUES (1, 'Halo Budi')");
        st.addBatch("INSERT INTO pesan VALUES (2, 'Stok tersedia')");
        st.addBatch("INSERT INTO pesan VALUES (3, 'Pesanan dikirim')");
        st.addBatch("INSERT INTO pesan VALUES (4, 'Terima kasih')");
        try {
            int[] hasil = st.executeBatch();
            System.out.println("hasil     : " + Arrays.toString(hasil));
        } catch (BatchGagalException e) {
            System.out.println("tidak terjadi di skenario ini: " + e.getMessage());
        }
        System.out.println("roundtrip : " + db.getRoundtrip());
        System.out.println("baris     : " + db.getJumlahBaris());

        System.out.println();
        System.out.println("== Batch dengan kegagalan + rollback ==");
        List<String[]> potret = db.snapshot();
        st = db.createStatement();
        st.addBatch("INSERT INTO pesan VALUES (5, 'Promo diskon')");
        st.addBatch("INSERT INTO pesan VALUES (1, 'Duplikat')");
        st.addBatch("INSERT INTO pesan VALUES (6, 'Ulasan produk')");
        try {
            st.executeBatch();
        } catch (BatchGagalException e) {
            System.out.println("gagal     : " + e.getMessage());
            System.out.println("getUpdateCounts : " + Arrays.toString(e.getUpdateCounts()));
            db.pulihkan(potret);
            System.out.println("[db] ROLLBACK: tabel dipulihkan ke potret awal");
        }
        System.out.println("baris     : " + db.getJumlahBaris());
    }
}

/** Database mini dengan penghitung roundtrip jaringan. */
class MiniDb {

    private final List<String[]> tabel = new ArrayList<>();
    private int roundtrip = 0;

    MiniStatement createStatement() {
        return new MiniStatement(this);
    }

    int getRoundtrip() {
        return roundtrip;
    }

    int getJumlahBaris() {
        return tabel.size();
    }

    void reset() {
        tabel.clear();
        roundtrip = 0;
    }

    List<String[]> snapshot() {
        return new ArrayList<>(tabel);
    }

    void pulihkan(List<String[]> potret) {
        tabel.clear();
        tabel.addAll(potret);
    }

    void catatRoundtrip() {
        roundtrip++;
    }

    int terapkan(String sql) throws KunciDuplikatException {
        Matcher m = Pattern
                .compile("INSERT INTO pesan VALUES \\((\\d+), '([^']+)'\\)")
                .matcher(sql);
        if (!m.matches()) {
            throw new IllegalArgumentException("SQL tidak dikenal: " + sql);
        }
        int id = Integer.parseInt(m.group(1));
        for (String[] baris : tabel) {
            if (Integer.parseInt(baris[0]) == id) {
                throw new KunciDuplikatException("primary key duplikat: id=" + id);
            }
        }
        tabel.add(new String[]{m.group(1), m.group(2)});
        System.out.println("[db] INSERT (" + id + ", '" + m.group(2) + "')");
        return 1;
    }
}

/** Statement mini dengan antrian batch. */
class MiniStatement {

    private final MiniDb db;
    private final List<String> antrian = new ArrayList<>();

    MiniStatement(MiniDb db) {
        this.db = db;
    }

    void addBatch(String sql) {
        antrian.add(sql);
    }

    void clearBatch() {
        antrian.clear();
    }

    int executeUpdate(String sql) {
        db.catatRoundtrip();                 // satu bolak-balik per pernyataan
        try {
            return db.terapkan(sql);
        } catch (KunciDuplikatException e) {
            throw new IllegalStateException(e.getMessage());
        }
    }

    int[] executeBatch() throws BatchGagalException {
        db.catatRoundtrip();                 // SELURUH antrian dalam satu bolak-balik
        System.out.println("[db] BATCH: " + antrian.size()
                + " pernyataan dikirim dalam 1 roundtrip");
        int[] hasil = new int[antrian.size()];
        for (int i = 0; i < antrian.size(); i++) {
            try {
                hasil[i] = db.terapkan(antrian.get(i));
            } catch (KunciDuplikatException e) {
                throw new BatchGagalException(
                        Arrays.copyOf(hasil, i), antrian.size(), e.getMessage());
            }
        }
        antrian.clear();
        return hasil;
    }
}

class KunciDuplikatException extends Exception {

    KunciDuplikatException(String pesan) {
        super(pesan);
    }
}

/** Padanan BatchUpdateException: membawa jumlah baris per pernyataan sukses. */
class BatchGagalException extends Exception {

    private final int[] updateCounts;

    BatchGagalException(int[] updateCounts, int total, String penyebab) {
        super(penyebab + " (" + updateCounts.length + " dari " + total
                + " pernyataan selesai sebelum gagal)");
        this.updateCounts = updateCounts;
    }

    int[] getUpdateCounts() {
        return updateCounts;
    }
}
