/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 1997, 1998
 *	Sleepycat Software.  All rights reserved.
 *
 *	@(#)TpcbExample.java	10.7 (Sleepycat) 12/7/98
 */

package com.sleepycat.examples;

import com.sleepycat.db.*;
import java.util.Calendar;
import java.util.Date;
import java.util.Random;
import java.util.GregorianCalendar;
import java.math.BigDecimal;

//
// This program implements a basic TPC/B driver program.  To create the
// TPC/B database, run with the -i (init) flag.  The number of records
// with which to populate the account, history, branch, and teller tables
// is specified by the a, s, b, and t flags respectively.  To run a TPC/B
// test, use the n flag to indicate a number of transactions to run (note
// that you can run many of these processes in parallel to simulate a
// multiuser test run).
//
class TpcbExample extends DbEnv
{
    // XXX Margo, check these ratios and record sizes.
    //
    public static final int TELLERS_PER_BRANCH = 100;
    public static final int ACCOUNTS_PER_TELLER = 1000;
    public static final int ACCOUNTS = 1000000;
    public static final int BRANCHES = 10;
    public static final int TELLERS = 1000;
    public static final int HISTORY = 1000000;
    public static final int HISTORY_LEN = 100;
    public static final int RECLEN = 100;
    public static final int BEGID = 1000000;

    // used by random_id()
    public static final int ACCOUNT = 0;
    public static final int BRANCH = 1;
    public static final int TELLER = 2;

    private static boolean verbose = false;
    private static final String progname = "TpcbExample";    // Program name.

    // Note: the constructor uses the default DbEnv() constructor,
    // which means that appinit() can be called after all options
    // have been set in the DbEnv.
    //
    public TpcbExample()
    {
        super();
    }

    //
    // Initialize the database to the specified number of accounts, branches,
    // history records, and tellers.
    //
    // Note: num_h was unused in the original ex_tpcb.c example.
    //
    public void
    populate(int num_a, int num_b, int num_h, int num_t)
    {
        Db dbp = null;

        int err;
        int balance, idnum;
        int end_anum, end_bnum, end_tnum;
        int start_anum, start_bnum, start_tnum;
        int h_nelem;

        idnum = BEGID;
        balance = 500000;

        DbInfo dbi = new DbInfo();

        h_nelem = num_a;
        dbi.set_h_nelem(h_nelem);

        try {
            dbp = Db.open("account",
                          Db.DB_HASH, Db.DB_CREATE | Db.DB_TRUNCATE, 0644, this, dbi);
        }
        catch (DbException dbe) {
            errExit(dbe, "Open of account file failed");
        }

        start_anum = idnum;
        populateTable(dbp, idnum, balance, h_nelem, "account");
        idnum += h_nelem;
        end_anum = idnum - 1;
        try {
            dbp.close(0);
        }
        catch (DbException dbe2) {
            errExit(dbe2, "Account file close failed");
        }

        if (verbose)
            System.out.println("Populated accounts: "
                 + String.valueOf(start_anum) + " - " + String.valueOf(end_anum));

        //
        // Since the number of branches is very small, we want to use very
        // small pages and only 1 key per page.  This is the poor-man's way
        // of getting key locking instead of page locking.
        //
        h_nelem = (int)num_b;
        dbi.set_h_nelem(h_nelem);
        dbi.set_h_ffactor(1);
        dbi.set_pagesize(512);

        try {
            dbp = Db.open("branch",
                          Db.DB_HASH, Db.DB_CREATE | Db.DB_TRUNCATE, 0644,
                          this, dbi);
        }
        catch (DbException dbe3) {
            errExit(dbe3, "Branch file create failed");
        }
        start_bnum = idnum;
        populateTable(dbp, idnum, balance, h_nelem, "branch");
        idnum += h_nelem;
        end_bnum = idnum - 1;

        try {
            dbp.close(0);
        }
        catch (DbException dbe4) {
            errExit(dbe4, "Close of branch file failed");
        }

        if (verbose)
            System.out.println("Populated branches: "
                 + String.valueOf(start_bnum) + " - " + String.valueOf(end_bnum));

        //
        // In the case of tellers, we also want small pages, but we'll let
        // the fill factor dynamically adjust itself.
        //
        h_nelem = (int)num_t;
        dbi.set_h_nelem(h_nelem);
        dbi.set_h_ffactor(0);
        dbi.set_pagesize(512);

        try {
            dbp = Db.open("teller",
                          Db.DB_HASH, Db.DB_CREATE | Db.DB_TRUNCATE, 0644, this, dbi);
        }
        catch (DbException dbe5) {
            errExit(dbe5, "Teller file create failed");
        }

        start_tnum = idnum;
        populateTable(dbp, idnum, balance, h_nelem, "teller");
        idnum += h_nelem;
        end_tnum = idnum - 1;

        try {
            dbp.close(0);
        }
        catch (DbException dbe6) {
            errExit(dbe6, "Close of teller file failed");
        }

        if (verbose)
            System.out.println("Populated tellers: "
                 + String.valueOf(start_tnum) + " - " + String.valueOf(end_tnum));

        // start with a fresh DbInfo
        //
        DbInfo histDbi = new DbInfo();

        histDbi.set_re_len(HISTORY_LEN);
        histDbi.set_flags(Db.DB_FIXEDLEN | Db.DB_RENUMBER);
        try {
            dbp = Db.open("history",
                          Db.DB_RECNO, Db.DB_CREATE | Db.DB_TRUNCATE, 0644,
                          this, histDbi);
        }
        catch (DbException dbe7) {
            errExit(dbe7, "Create of history file failed");
        }

        populateHistory(dbp, num_h, num_a, num_b, num_t);

        try {
            dbp.close(0);
        }
        catch (DbException dbe8) {
            errExit(dbe8, "Close of history file failed");
        }
    }

    public void
    populateTable(Db dbp,
                  int start_id, int balance,
                  int nrecs, String msg)
    {
        Defrec drec = new Defrec();

        Dbt kdbt = new Dbt(drec.data);
        kdbt.set_size(4);                  // sizeof(int)
        Dbt ddbt = new Dbt(drec.data);
        ddbt.set_size(drec.data.length);   // uses whole array

        try {
            for (int i = 0; i < nrecs; i++) {
                kdbt.set_recno_key_data(start_id + (int)i);
                drec.set_balance(balance);
                dbp.put(null, kdbt, ddbt, Db.DB_NOOVERWRITE);
            }
        }
        catch (DbException dbe) {
            System.err.println("Failure initializing " + msg + " file: " +
                               dbe.toString());
            System.exit(1);
        }
    }

    public void
    populateHistory(Db dbp, int nrecs,
                                 int anum, int bnum, int tnum)
    {
        Histrec hrec = new Histrec();
        hrec.set_amount(10);

        byte arr[] = new byte[4];                  // sizeof(int)
        int i;
        Dbt kdbt = new Dbt(arr);
        kdbt.set_size(arr.length);
        Dbt ddbt = new Dbt(hrec.data);
        ddbt.set_size(hrec.data.length);

        try {
            for (i = 1; i <= nrecs; i++) {
                kdbt.set_recno_key_data(i);

                hrec.set_aid(random_id(ACCOUNT, anum, bnum, tnum));
                hrec.set_bid(random_id(BRANCH, anum, bnum, tnum));
                hrec.set_tid(random_id(TELLER, anum, bnum, tnum));

                dbp.put(null, kdbt, ddbt, Db.DB_APPEND);
            }
        }
        catch (DbException dbe) {
            errExit(dbe, "Failure initializing history file");
        }
    }

    static Random rand = new Random();

    public static int
    random_int(int lo, int hi)
    {
        int ret;
        int t;

        t = rand.nextInt();
        if (t < 0)
            t = -t;
        ret = (int)(((double)t / ((double)(Integer.MAX_VALUE) + 1)) *
                          (hi - lo + 1));
        ret += lo;
        return (ret);
    }

    public static int
    random_id(int type, int accounts, int branches, int tellers)
    {
        int min, max, num;

        max = min = BEGID;
        num = accounts;
        switch(type) {
        case TELLER:
            min += branches;
            num = tellers;
            // Fallthrough
        case BRANCH:
            if (type == BRANCH)
                num = branches;
            min += accounts;
            // Fallthrough
        case ACCOUNT:
            max = min + num - 1;
        }
        return (random_int(min, max));
    }

    public void
    run(int n, int accounts, int branches, int tellers)
    {
        Db adb = null;
        Db bdb = null;
        Db hdb = null;
        Db tdb = null;
        double gtps, itps;
        int failed, ifailed, ret, txns;
        long gus, ius;
        long starttime, curtime, lasttime;

        //
        // Open the database files.
        //
        int err;
        try {
            adb = Db.open("account", Db.DB_UNKNOWN, 0, 0, this, null);
            bdb = Db.open("branch", Db.DB_UNKNOWN, 0, 0, this, null);
            tdb = Db.open("teller", Db.DB_UNKNOWN, 0, 0, this, null);
            hdb = Db.open("history", Db.DB_UNKNOWN, 0, 0, this, null);
        }
        catch (DbException dbe) {
            errExit(dbe, "Open of db files failed");
        }

        txns = failed = ifailed = 0;
        starttime = (new Date()).getTime();
        lasttime = starttime;
        while (n-- >= 0) {
            txns++;
            DbTxnMgr txnmgr = get_tx_info();
            ret = txn(txnmgr, adb, bdb, tdb, hdb, accounts, branches, tellers);
            if (ret != 0) {
                failed++;
                ifailed++;
            }
            if (n % 1000 == 0) {
                curtime = (new Date()).getTime();
                gus = (curtime - starttime) * 1000;
                ius = (curtime - lasttime) * 1000;
                gtps = (double)(txns - failed) /
                    ((double)gus / 1000000);
                itps = (double)(1000 - ifailed) /
                    ((double)ius / 1000000);
                System.out.print(String.valueOf(txns) + " txns " +
                                 String.valueOf(failed) + " failed ");
                System.out.println(showRounded(gtps, 2) + " TPS (gross) " +
                                   showRounded(itps, 2) + " TPS (interval)");
                lasttime = curtime;
                ifailed = 0;
            }
        }

        try {
            adb.close(0);
            bdb.close(0);
            tdb.close(0);
            hdb.close(0);
        }
        catch (DbException dbe2) {
            errExit(dbe2, "Close of db files failed");
        }

        System.out.println((long)txns + " transactions begun "
             + String.valueOf(failed) + " failed");

    }

    //
    // XXX Figure out the appropriate way to pick out IDs.
    //
    public int
    txn(DbTxnMgr txmgr,
        Db adb, Db bdb, Db tdb, Db hdb,
        int anum, int bnum, int tnum)
    {
        Dbc acurs = null;
        Dbc bcurs = null;
        Dbc hcurs = null;
        Dbc tcurs = null;
        DbTxn t = null;

        Defrec rec = new Defrec();
        Histrec hrec = new Histrec();
        int account, branch, teller;

        Dbt d_dbt = new Dbt();
        Dbt d_histdbt = new Dbt();
        Dbt k_dbt = new Dbt();
        Dbt k_histdbt = new Dbt();

        account = random_id(ACCOUNT, anum, bnum, tnum);
        branch = random_id(BRANCH, anum, bnum, tnum);
        teller = random_id(TELLER, anum, bnum, tnum);

        // The history key will not actually be retrieved,
        // but it does need to be set to something.
        byte hist_key[] = new byte[4];
	k_histdbt.set_data(hist_key);
	k_histdbt.set_size(4 /* == sizeof(int)*/);

        byte key_bytes[] = new byte[4];
        k_dbt.set_data(key_bytes);
        k_dbt.set_size(4 /* == sizeof(int)*/);

        d_dbt.set_flags(Db.DB_DBT_USERMEM);
        d_dbt.set_data(rec.data);
        d_dbt.set_ulen(rec.length());

        hrec.set_aid(account);
        hrec.set_bid(branch);
        hrec.set_tid(teller);
        hrec.set_amount(10);
        // Request 0 bytes since we're just positioning.
        d_histdbt.set_flags(Db.DB_DBT_PARTIAL);

        // START TIMING

        try {
            t = txmgr.begin(null);

            acurs = adb.cursor(t, 0);
            bcurs = bdb.cursor(t, 0);
            tcurs = tdb.cursor(t, 0);
            hcurs = hdb.cursor(t, 0);

            // Account record
            k_dbt.set_recno_key_data(account);
            if (acurs.get(k_dbt, d_dbt, Db.DB_SET) != 0)
                throw new TpcbException("acurs get failed");
            rec.set_balance(rec.get_balance() + 10);
            acurs.put(k_dbt, d_dbt, Db.DB_CURRENT);

            // Branch record
            k_dbt.set_recno_key_data(branch);
            if (bcurs.get(k_dbt, d_dbt, Db.DB_SET) != 0)
                throw new TpcbException("bcurs get failed");
            rec.set_balance(rec.get_balance() + 10);
            bcurs.put(k_dbt, d_dbt, Db.DB_CURRENT);

            // Teller record
            k_dbt.set_recno_key_data(teller);
            if (tcurs.get(k_dbt, d_dbt, Db.DB_SET) != 0)
                throw new TpcbException("ccurs get failed");
            rec.set_balance(rec.get_balance() + 10);
            tcurs.put(k_dbt, d_dbt, Db.DB_CURRENT);

            // History record
            d_histdbt.set_flags(0);
            d_histdbt.set_data(hrec.data);
            d_histdbt.set_ulen(hrec.length());
            if (hdb.put(t, k_histdbt, d_histdbt, Db.DB_APPEND) != 0)
		throw(new DbException("put failed"));

            acurs.close();
            bcurs.close();
            tcurs.close();
            hcurs.close();

            t.commit();

            // END TIMING
            return (0);

        }
        catch (Exception e) {
            try {
                if (acurs != null)
                    acurs.close();
                if (bcurs != null)
                    bcurs.close();
                if (tcurs != null)
                    tcurs.close();
                if (hcurs != null)
                    hcurs.close();
                if (t != null)
                    t.abort();
            }
            catch (DbException dbe) {
                // not much we can do here.
            }

            if (verbose) {
                System.out.println("Transaction A=" + String.valueOf(account)
                                   + " B=" + String.valueOf(branch)
                                   + " T=" + String.valueOf(teller) + " failed");
                System.out.println("Reason: " + e.toString());
            }
            return (-1);
        }
    }

    static void errExit(DbException err, String s)
    {
        System.err.print(progname + ": ");
        if (s != null) {
            System.err.print(s + ": ");
        }
        System.err.println(err.toString());
        System.exit(1);
    }


    public static void main(String argv[])
    {
        long seed;
        int accounts, branches, tellers, history;
        boolean iflag;
        int mpool, ntxns;
        String home, endarg;

        home = null;
        accounts = branches = history = tellers = 0;
        mpool = ntxns = 0;
        verbose = false;
        iflag = false;
        seed = (new GregorianCalendar()).get(Calendar.SECOND);

        for (int i = 0; i < argv.length; ++i)
        {
            if (argv[i].equals("-a")) {
                // Number of account records
                if ((accounts = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            }
            else if (argv[i].equals("-b")) {
                // Number of branch records
                if ((branches = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            }
            else if (argv[i].equals("-h")) {
                // DB  home.
                home = argv[++i];
            }
            else if (argv[i].equals("-i")) {
                // Initialize the test.
                iflag = true;
            }
            else if (argv[i].equals("-m")) {
                // Bytes in buffer pool
                if ((mpool = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            }
            else if (argv[i].equals("-n")) {
                // Number of transactions
                if ((ntxns = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            }
            else if (argv[i].equals("-S")) {
                // Random number seed.
                seed = Long.parseLong(argv[++i]);
                if (seed <= 0)
                    invarg(argv[i]);
            }
            else if (argv[i].equals("-s")) {
                // Number of history records
                if ((history = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            }
            else if (argv[i].equals("-t")) {
                // Number of teller records
                if ((tellers = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            }
            else if (argv[i].equals("-v")) {
                // Verbose option.
                verbose = true;
            }
            else
            {
                usage();
            }
        }

        rand.setSeed((int)seed);

        accounts = accounts == 0 ? ACCOUNTS : accounts;
        branches = branches == 0 ? BRANCHES : branches;
        tellers = tellers == 0 ? TELLERS : tellers;
        history = history == 0 ? HISTORY : history;

        if (verbose)
            System.out.println((long)accounts + " Accounts "
                 + String.valueOf(branches) + " Branches "
                 + String.valueOf(tellers) + " Tellers "
                 + String.valueOf(history) + " History");

        // Declaring and setting options does not need
        // to be done in a try block, as it will never
        // raise an exception.
        //
        TpcbExample app = new TpcbExample();
        int flags = Db.DB_CREATE | Db.DB_INIT_MPOOL;

        app.set_error_stream(System.err);
        app.set_errpfx("TpcbExample");

        if (mpool == 0) {
            mpool = 4 * 1024 * 1024;
        }
        app.set_mp_size(mpool);

        if (!iflag) {
            flags |= Db.DB_INIT_TXN | Db.DB_INIT_LOCK | Db.DB_INIT_LOG;
        }

        // Initialize the database environment.
        // Must be done in within a try block, unless you
        // change the error model in the options.
        //
        try {
            app.appinit(home, null, flags);
        }
        catch (DbException dbe1) {
            errExit(dbe1, "appinit failed");
        }

        if (iflag && ntxns != 0) {
            System.err.println("specify only one of -i and -n");
            System.exit(1);
        }
        if (iflag)
            app.populate(accounts, branches, history, tellers);
        if (ntxns != 0)
            app.run(ntxns, accounts, branches, tellers);

        // Shut down the application.

        try {
            app.appexit();
        }
        catch (DbException dbe2) {
            errExit(dbe2, "appexit failed");
        }

        System.exit(0);
    }

    private static void invarg(String str)
    {
        System.err.println("TpcbExample: invalid argument: " + str);
        System.exit(1);
    }

    private static void usage()
    {
        System.err.println("usage: TpcbExample "
             + "[-iv] [-a accounts] [-b branches] [-h home] [-m mpool_size] "
             + "[-n transactions ] [-S seed] [-s history] [-t tellers] ");
        System.exit(1);
    }

    // round 'd' to 'scale' digits, and return result as string
    private String showRounded(double d, int scale)
    {
        return new BigDecimal(d).
            setScale(scale, BigDecimal.ROUND_HALF_DOWN).toString();
    }

    // The byte order is our choice.
    //
    static long get_int_in_array(byte[] array, int offset)
    {
        return
            ((0xff & array[offset+0]) << 0)  |
            ((0xff & array[offset+1]) << 8)  |
            ((0xff & array[offset+2]) << 16) |
            ((0xff & array[offset+3]) << 24);
    }

    // Note: Value needs to be long to avoid sign extension
    static void set_int_in_array(byte[] array, int offset, long value)
    {
        array[offset+0] = (byte)((value >> 0) & 0x0ff);
        array[offset+1] = (byte)((value >> 8) & 0x0ff);
        array[offset+2] = (byte)((value >> 16) & 0x0ff);
        array[offset+3] = (byte)((value >> 24) & 0x0ff);
    }

};

// Simulate the following C struct:
// struct Defrec {
//     u_int32_t   id;
//     u_int32_t   balance;
//     u_int8_t    pad[RECLEN - sizeof(int) - sizeof(int)];
// };

class Defrec
{
    public Defrec()
    {
        data = new byte[TpcbExample.RECLEN];
    }

    public int length()
    {
        return TpcbExample.RECLEN;
    }

    public long get_id()
    {
        return TpcbExample.get_int_in_array(data, 0);
    }

    public void set_id(long value)
    {
        TpcbExample.set_int_in_array(data, 0, value);
    }

    public long get_balance()
    {
        return TpcbExample.get_int_in_array(data, 4);
    }

    public void set_balance(long value)
    {
        TpcbExample.set_int_in_array(data, 4, value);
    }

    static {
        Defrec d = new Defrec();
        d.set_balance(500000);
    }

    public byte[] data;
}

// Simulate the following C struct:
// struct Histrec {
//     u_int32_t   aid;
//     u_int32_t   bid;
//     u_int32_t   tid;
//     u_int32_t   amount;
//     u_int8_t    pad[RECLEN - 4 * sizeof(u_int32_t)];
// };

class Histrec
{
    public Histrec()
    {
        data = new byte[TpcbExample.RECLEN];
    }

    public int length()
    {
        return TpcbExample.RECLEN;
    }

    public long get_aid()
    {
        return TpcbExample.get_int_in_array(data, 0);
    }

    public void set_aid(long value)
    {
        TpcbExample.set_int_in_array(data, 0, value);
    }

    public long get_bid()
    {
        return TpcbExample.get_int_in_array(data, 4);
    }

    public void set_bid(long value)
    {
        TpcbExample.set_int_in_array(data, 4, value);
    }

    public long get_tid()
    {
        return TpcbExample.get_int_in_array(data, 8);
    }

    public void set_tid(long value)
    {
        TpcbExample.set_int_in_array(data, 8, value);
    }

    public long get_amount()
    {
        return TpcbExample.get_int_in_array(data, 12);
    }

    public void set_amount(long value)
    {
        TpcbExample.set_int_in_array(data, 12, value);
    }

    public byte[] data;
}

class TpcbException extends Exception
{
    TpcbException()
    {
        super();
    }

    TpcbException(String s)
    {
        super(s);
    }
}
