/* ----------------------------------------------------------------------------
 *
 * Raima Database Manager
 *
 * Copyright (c) 2012 Raima Inc.,  All rights reserved.
 *
 * Use of this software, whether in source code format, or in executable,
 * binary object code form, is governed by the Raima LICENSE which
 * is fully described in the LICENSE.TXT file, included within this
 * distribution of files.
 *
 * --------------------------------------------------------------------------*/
#include <stdio.h>

/* rdm.h is required in all files using the RDM Database */
#include "rdm.h"

/* perf07_db.h was generated from the DDL file perf07_db.sdl. */
#include "perf07_db.h"

/* perf07_db_dbd.h was generated from the DDL file perf07_db.ddl. using ddlp with the -c option */
#include "perf07_db_dbd.h"

/*! Number of levels in the BOM */
#define NUM_LEVELS 6

/*! Number of members per level in the BOM */
#define NUM_MEMBERS 8

/*! Context structure for the Perf07 example */
typedef struct _PERF07_CTX
{
    int32_t         current_level; /*!< Level currently being processed */
    int32_t         max_level;     /*!< Maximum BOM level */
    int32_t         max_members;   /*!< Maximum number of members per BOM level */
    int32_t         count;         /*!< Count of BOM members */
    TFS_HANDLE      hTFS;          /*!< RDM TFS handle */
    DB_TASK        *task;          /*!< RDM task */
} PERF07_CTX;

static int32_t build_bill(PERF07_CTX *);


/*! \file perf07.c
 * \brief Source code for the RDM Perf07 example
 */
 
/*!
 * \mainpage Perf07
 * \author Jeffrey R. Parsons, Raima Sr. Engineer
 * \date 2012
 *
 * \section Description
 * EXAMPLE - Perf07
 *
 * The Perf07 example creates and reads a Bill-of-Materials using network
 * model sets.
 *
 * Each time you run this example
 *   * The database is initialized (removing all existing data)
 *   * A new Bill-of-Materials is created
 *   * The Bill-of-Materials is read
 *   * Database is cleaned up
 *
 * \section Perf07 Database
 * The DDL (Database Definition Language) specification for the database used
 * in this example is located in the file perf07_db.ddl. The example utilizes 
 * the RDM Core DDL syntax and specifies a database suitable for storing a
 * Bill-of-Materials.
 *
 * \include perf07_db.ddl
 *
 * The schema was compiled using the RDM ddlp utility with the -c option to
 * generate an embedded database dictionary (used by the RDM d_open_ptr API).
 *
 * \code ddlp -c -d perf07_db_db.ddl \endcode
 *
 * The database dictionary information is embedded inside the application so 
 * an external DBD file stored on the file system is not required.
 *
 * \section RDM Functions Used
 * The following RDM functions are illustrated:
 *   * d_tfsinitEx/d_tfsterm
 *   * d_errorname
 *   * d_errorstr
 *   * d_opentask/d_closetask
 *   * d_open_ptr/d_close
 *   * d_initialize
 *   * d_csoget
 *   * d_csmget
 *   * d_setor
 *   * d_fillnew
 *   * d_connect
 *   * d_setro
 *   * d_csoset
 *   * d_csmset
 *   * d_members
 *   * d_recread
 *   * d_findnm
 *   * d_findco
 *   * d_recfrst
 *
 * They are fully documented in http://docs.raima.com/rdm/.
 */

/*! \brief Display a message and the elapsed time to perform database operations
 *
 */
void display_elapsed_time(
    const char *msg,         /**< [in] Message to be display along with the elapsed time */
    uint64_t    start_time,  /**< [in] Start of elapsed time to display */
    uint64_t    end_time)    /**< [in] End of elapsed */
{
    uint64_t elapsed_time = end_time - start_time;
    printf("%s" UINT64_SPEC " millisecs\n", msg, elapsed_time/1000000);
}

/*! \brief Initialize the RDM runtime library for use in the perf07 example
 *
 * This function initializes the RDM Transactional File Server (TFS) to use 
 * the STANDALONE TFS implementation.  It also creates
 * an RDM runtime task and opens the "perf07_db" database in exclusive ('x') mode.
 * Exclusve mode does not require database locks or transactions.
 *
 * @return Returns a 32-bit RDM return code (S_OKAY if successful)
 */
int32_t initialize(
	PERF07_CTX  *ctx)    /**< [in/out] Pointer to the application context */
{
    int32_t  rdm_status = S_OKAY;
    uint64_t start_time;
    uint64_t end_time;    

    /* Initialize the TFS to use the single-user configuration (STANDALONE) */    
    start_time = psp_timeNanoSecs();
    rdm_status = d_tfsinitEx(NULL, NULL, &ctx->hTFS, TFS_TYPE_STANDALONE);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error initializing the TFS (%s - %s)\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    /* Create the RDM DB_TASK */
    rdm_status = d_opentask(&ctx->task);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error opening the RDM task (%s - %s)\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    /* Open the perf07_db database in exclusive mode (does not require locks or tranactions) */
    rdm_status = d_open_ptr("perf07_db", "x", &perf07_db_dbd, sizeof(perf07_db_dbd), ctx->task);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error opening the perf07_db database (%s - %s)\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    /* Initialize the database (creates new database files truncating any existing data) */    
    rdm_status = d_initialize(ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error initializing the perf07_db database (%s - %s)\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }
    end_time = psp_timeNanoSecs();

	ctx->max_level = NUM_LEVELS;
	ctx->max_members = NUM_MEMBERS;
    
    /* Display information */
    printf("\n");
    printf("Initializing with STANDALONE configuration\n");
    display_elapsed_time("Preparing a new database:\t\t\t", start_time, end_time);
    printf("\tDisk-based\n");
    printf("\tOpened exclusively\n");

    return rdm_status;
}

/*! \brief Cleanup the RDM runtime library
 *
 * This functions closes all open databases and cleans up the RDM runtime task
 * used in the perf07 example.  It also terminates the TFS used
 * in the example. 
 *
 * @return Returns a 32-bit RDM return code (S_OKAY if successful)
 */
void cleanup(
    PERF07_CTX *ctx)   /**< [in] Pointer to the application context */
{
    uint64_t start_time;
    uint64_t end_time;

    start_time = psp_timeNanoSecs();
    if(ctx->task)
    {
        /* Close any open databases */
        d_close(ctx->task);

        /* Close the task */
        d_closetask(ctx->task);
    }

    /* Terminate the TFS */
    d_tfsterm(ctx->hTFS);

    end_time = psp_timeNanoSecs();

    printf("\n");
    display_elapsed_time("Cleanup:\t\t\t\t\t", start_time, end_time);
}

/*! \brief Generate a 15-character alpha-numeric part id
 *
 * This function generates a part id for an item 
 *
 */
void bom_random_id(
    PERF07_CTX *ctx,        /**< [in] Pointer to the application context */
    char       *string)     /**< [out] 15-character alpha part id */
{
    sprintf(string, "%-15.15d", ++(ctx->count));
}

/*! \brief Recursive rountine to build one level of a bill by adding components to a parent
 *
 * This function recursively builds a full bill of materials in the Core07
 * database.
 *
 * @return Returns a 32-bit RDM return code (S_OKAY if successful)
 */
int32_t build_bill(
    PERF07_CTX *ctx)       /**< [in] Pointer to the application context */
{
    int32_t     rdm_status;
    struct bill bill_rec;
    DB_ADDR     bom_owner = NULL_DBA;
    DB_ADDR     bom_member = NULL_DBA;
    int32_t     i;
    struct item item_rec;

    ctx->current_level++;

    /* get current owner and member of BOM set - restore these before
       the function returns, so that the currency will be correct for
       the previous level of recursion
    */
    rdm_status = d_csoget(BOM, &bom_owner, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        /* We may not have a current owner yet */
        if(rdm_status == S_NOCO)
        {
            bom_owner = NULL_DBA;
            rdm_status = S_OKAY;
        }
        else
        {
            fprintf(stderr, "Error getting current BOM owner: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }
    }

    rdm_status = d_csmget(BOM, &bom_member, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        /* We may not have a current member yet */
        if(rdm_status == S_NOCM)
        {
            bom_member = NULL_DBA;
            rdm_status = S_OKAY;
        }
        else
        {
            fprintf(stderr, "Error getting current BOM member: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }
    }

    /* make the current record the current owner of BOM set */
    rdm_status = d_setor(BOM, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error setting current BOM owner: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    for (i=0; i<ctx->max_members; i++) {

        /* create new ITEM record, for component item */
        item_rec.cost = 1.0L;
        bom_random_id(ctx, (char *)(item_rec.id_code));
        if (d_fillnew(ITEM, &item_rec, ctx->task, CURR_DB) != S_OKAY) {    /* new component ITEM record */
            printf("duplicate part %s\n", item_rec.id_code);
        }

        rdm_status = d_setor(WHERE_USED, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error setting current WHERE_USED owner: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        /* create new BILL record */
        bill_rec.quantity = 1.0L;
        bill_rec.level = ctx->current_level;
        rdm_status = d_fillnew(BILL, &bill_rec, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error creating new BILL record: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        /* connect new BILL record to parent ITEM and component ITEM,
           to form a linkage between the two items, in a many-to-many
           relationship
        */
        rdm_status = d_connect(BOM, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error connecting BOM set: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        rdm_status = d_connect(WHERE_USED, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error connecting WHERE_USED set: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        /* if we are not at the bottom level, recurse down another level */
        if (ctx->current_level < ctx->max_level) 
        {

            /* make the new component ITEM record the current record,
               so that the recursive call to this function will connect
               further components to it
            */
            rdm_status = d_setro(WHERE_USED, ctx->task, CURR_DB);
            if(rdm_status != S_OKAY)
            {
                fprintf(stderr, "Error setting current record from WHERE_USED owner: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
                return rdm_status;
            }

            rdm_status = build_bill(ctx);
            if(rdm_status != S_OKAY)
            {
                return rdm_status;
            }
        }
    }

    ctx->current_level--;

    /* restore original currency */
    rdm_status = d_csoset(BOM, &bom_owner, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error setting BOM owner: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    rdm_status = d_csmset(BOM, &bom_member, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error setting BOM member: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }
    return rdm_status;
}

/*! \brief Top level function to create a BOM
 *
 * This function recursively builds a full bill of materials in the Core07
 * database.
 *
 * @return Returns a 32-bit RDM return code (S_OKAY if successful)
 */
int32_t create_bom(
    PERF07_CTX *ctx) /**< [in] Pointer to the application context */
{
    int32_t      rdm_status;
    struct  item item_rec;
    char         id_code[SIZEOF_ID_CODE];
    uint64_t start_time;
    uint64_t end_time;

    start_time = psp_timeNanoSecs();

    item_rec.id_code[0] = '\0';
    item_rec.cost = 1.0L;

    /* create top level ITEM record */
    bom_random_id(ctx, id_code);
    strcpy((char *)item_rec.id_code,  (char *)id_code);

    if ((rdm_status = d_fillnew(ITEM, &item_rec, ctx->task, CURR_DB)) != S_OKAY) 
    {
		fprintf(stderr, "Error creating ITEM record: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    /* recursive call to build multi-level bill */
    rdm_status = build_bill(ctx);

    end_time = psp_timeNanoSecs();

    display_elapsed_time("\t\t", start_time, end_time);
    return rdm_status;
}

/*! \brief Recursive rountine roll up cost from lower levels of a BOM
 *
 * This function recursively rolls-up the cost of a BOM.  The costs are
 * only stored at the lowest levels of the BOM.
 *
 * @return Returns a 32-bit RDM return code (S_OKAY if successful)
 */

int32_t get_cost(
    double     *total_cost,  /**< [out] Pointer to area to put the total cost */
    PERF07_CTX *ctx)         /**< [in] Pointer to the application context */
{
    int32_t     rdm_status;
    DB_ADDR     bom_owner;
    DB_ADDR     bom_member;
    int32_t     member_count;
    struct bill Bill_local;
    struct item item_rec;
    double      dCost;
    double      dTotalCost;

    /* get current owner and member of BOM set - restore these before
       the function returns, so that the currency will be correct for
       the previous level of recursion
    */
    rdm_status = d_csoget(BOM, &bom_owner, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        /* We may not have a current member yet */
        if(rdm_status == S_NOCO)
        {
            bom_member = NULL_DBA;
            rdm_status = S_OKAY;
        }
        else
        {
            fprintf(stderr, "Error getting current BOM member: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }
    }

    rdm_status = d_csmget(BOM, &bom_member, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        /* We may not have a current member yet */
        if(rdm_status == S_NOCM)
        {
            bom_member = NULL_DBA;
            rdm_status = S_OKAY;
        }
        else
        {
            fprintf(stderr, "Error %d: getting current BOM member\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }
    }

    /* make the current record the current owner of BOM set */
    rdm_status = d_setor(BOM, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error setting current BOM owner\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    /* get number of components connected to this ITEM record */
    rdm_status = d_members(BOM, &member_count, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error getting number of BOM members\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    /* no members - we are at the bottom level now */
    if (member_count == 0) {

        /* read the current ITEM to get cost */
        rdm_status = d_recread(&item_rec, ctx->task, CURR_DB);

        /* restore original currency */
        rdm_status = d_csoset(BOM, &bom_owner, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error setting current BOM owner\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        rdm_status = d_csmset(BOM, &bom_member, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error setting current BOM member\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        /* return cost from the ITEM record just read */
        *total_cost = item_rec.cost;
        return rdm_status;
    }

    /* there is at least one member, so go down a level */
    dTotalCost = 0.0L;

    /* cycle through all components */
    while (member_count--) 
    {

        /* find next member - if there's no current member this will
           find the first member
        */
        rdm_status = d_findnm(BOM, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error getting next BOM member\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        /* read the bill rec to get quanity */
        rdm_status = d_recread(&Bill_local, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error reading BILL record\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        /* find its owner thru WHERE_USED set - this is a component ITEM */
        rdm_status = d_findco(WHERE_USED, ctx->task, CURR_DB);
        if(rdm_status != S_OKAY)
        {
            fprintf(stderr, "Error finding WHERE_USED owner\n", d_errorname(rdm_status), d_errorstr(rdm_status));
            return rdm_status;
        }

        /* recursive call - get cost of all components connected to this one */
        rdm_status = get_cost(&dCost, ctx);
        if(rdm_status == S_OKAY)
        {
            dTotalCost += (dCost * Bill_local.quantity);
        }
        else
        {
            return rdm_status;
        }
    }

    /* restore original currency */
    rdm_status = d_csoset(BOM, &bom_owner, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error setting current BOM owner: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    rdm_status = d_csmset(BOM, &bom_member, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error setting current BOM member: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }

    *total_cost = dTotalCost;
    return rdm_status;
}

/*! \brief Top level function for rolling the BOM
 *
 * This function recursively rolls up and calculate the full cost
 * of a BOM.
 *
 * @return Returns a 32-bit RDM return code (S_OKAY if successful)
 */
int32_t rollup_bom(
    PERF07_CTX *ctx) /**< [in] Pointer to the application context */
{
    int32_t  rdm_status;
    double   total_cost;
    uint64_t start_time;
    uint64_t end_time;

    start_time = psp_timeNanoSecs();

    /* Navigate the top level ITEM record */
    rdm_status = d_recfrst(ITEM, ctx->task, CURR_DB);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Error navigating to first ITEM record: %s - %s\n", d_errorname(rdm_status), d_errorstr(rdm_status));
        return rdm_status;
    }    

    rdm_status = get_cost(&total_cost, ctx);
    end_time = psp_timeNanoSecs();
    
    printf("\tTotal cost is $%10.02lf", total_cost);
    display_elapsed_time("\t\t", start_time, end_time);
    
    return rdm_status;
}

/*! \brief Main function for perf07 example
 *
 * The function initializes the RDM environment and runs the insert operations.
 * over a number of transaction block sizes
 *
 * @return Returns 0 on success, and 1 if there were any errors
 */
int32_t main()
{
    PERF07_CTX  ctx;
    int32_t     rdm_status;

    /* Display the example title */
    printf("%s", RDM_EXAMPLE_DESC("Perf07"));

    memset(&ctx, 0, sizeof(PERF07_CTX));

    /* Initialize the TFS, task, and open/initialize the database */
    rdm_status = initialize(&ctx);
    if(rdm_status != S_OKAY)
    {
        fprintf(stderr, "Application is aborting due to initialization error\n");
        cleanup(&ctx);
        return 1;
    }

    /* Build the BOM */
    printf("\nCreating BOM with %d levels and %d members per level\n", ctx.max_level, ctx.max_members);
    printf("\tBuilding bill of materials");
    create_bom(&ctx);
    
    /* Read the BOM - rollup cost */
    printf("\nRolling up BOM cost\n");
    rollup_bom(&ctx);

    cleanup(&ctx);

    printf("\nHit <Enter> to continue . . .");
    getchar();

	return 0;
}
