/*
 * Copyright (C) 2010-2013 Volodymyr Tarasenko <tvntsr@yahoo.com>
 *              2010      Sergey Pavlov <sergey.pavlov@gmail.com>
 *              2010      PortaOne Inc.
 * Copyright (C) Tildeslash Ltd. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */ 

#include "Config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <oci.h>

#include "URL.h"
#include "ResultSet.h"
#include "StringBuffer.h"
#include "PreparedStatement.h"
#include "OracleResultSet.h"
#include "OraclePreparedStatement.h"
#include "ConnectionDelegate.h"
#include "OracleConnection.h"


/**
 * Implementation of the Connection/Delegate interface for oracle. 
 * 
 * TODO: Query Timeout has no effect as this is not implemented. 
 *
 * @file
 */


/* ----------------------------------------------------------- Definitions */

const struct Cop_T oraclesqlcops = {
        .name 		 	= "oracle",
        .new 		 	= OracleConnection_new,
        .free 		 	= OracleConnection_free,
        .setQueryTimeout 	= OracleConnection_setQueryTimeout,
        .setMaxRows 	 	= OracleConnection_setMaxRows,
        .ping		 	= OracleConnection_ping,
        .beginTransaction       = OracleConnection_beginTransaction,
        .commit			= OracleConnection_commit,
        .rollback		= OracleConnection_rollback,
        .lastRowId		= OracleConnection_lastRowId,
        .rowsChanged		= OracleConnection_rowsChanged,
        .execute		= OracleConnection_execute,
        .executeQuery		= OracleConnection_executeQuery,
        .prepareStatement	= OracleConnection_prepareStatement,
        .getLastError		= OracleConnection_getLastError
};

#define ERB_SIZE 152
#define ORACLE_TRANSACTION_PERIOD 10

#define T ConnectionDelegate_T
struct T {
        URL_T          url;
        OCIEnv*        env;
        OCIError*      err;
        OCISvcCtx*     svc;
        OCISession*    usr;
        OCIServer*     srv;
        OCITrans*      txnhp;
        char           erb[ERB_SIZE];
        int            maxRows;
        int            timeout;
        sword          lastError;
        ub4            rowsChanged;
        StringBuffer_T sb;
};

extern const struct Rop_T oraclerops;
extern const struct Pop_T oraclepops;


/* ------------------------------------------------------- Private methods */


static int _doConnect(T C, URL_T url, char**  error) {
#define ERROR(e) do {*error = Str_dup(e); return false;} while (0)
#define ORAERROR(e) do{ *error = Str_dup(OracleConnection_getLastError(e)); return false;} while(0)
        const char *database, *username, *password;
        const char *host = URL_getHost(url);
        int port = URL_getPort(url);
        if (! (username = URL_getUser(url)))
                if (! (username = URL_getParameter(url, "user")))
                        ERROR("no username specified in URL");
        if (! (password = URL_getPassword(url)))
                if (! (password = URL_getParameter(url, "password")))
                        ERROR("no password specified in URL");
        if (! (database = URL_getPath(url)))
                ERROR("no database specified in URL");
        ++database;
        /* Create a thread-safe OCI environment with N' substitution turned on. */
        if (OCIEnvCreate(&C->env, OCI_THREADED | OCI_OBJECT | OCI_NCHAR_LITERAL_REPLACE_ON, 0, 0, 0, 0, 0, 0))
                ERROR("Create a OCI environment failed");
        /* allocate an error handle */
        if (OCI_SUCCESS != OCIHandleAlloc(C->env, (dvoid**)&C->err, OCI_HTYPE_ERROR, 0, 0))
                ERROR("Allocating error handler failed");
        /* server contexts */
        if (OCI_SUCCESS != OCIHandleAlloc(C->env, (dvoid**)&C->srv, OCI_HTYPE_SERVER, 0, 0))
                ERROR("Allocating server context failed");
        /* allocate a service handle */
        if (OCI_SUCCESS != OCIHandleAlloc(C->env, (dvoid**)&C->svc, OCI_HTYPE_SVCCTX, 0, 0))
                ERROR("Allocating service handle failed");
        StringBuffer_clear(C->sb);
        /* Oracle connect string is on the form: //host[:port]/service name */
        if (host) {
                StringBuffer_append(C->sb, "//%s", host);
                if (port > 0)
                        StringBuffer_append(C->sb, ":%d", port);
                StringBuffer_append(C->sb, "/%s", database);
        } else /* Or just service name */
                StringBuffer_append(C->sb, "%s", database);
        /* Create a server context */
        C->lastError = OCIServerAttach(C->srv, C->err, StringBuffer_toString(C->sb), StringBuffer_length(C->sb), 0);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                ORAERROR(C);
        /* Set attribute server context in the service context */
        C->lastError = OCIAttrSet(C->svc, OCI_HTYPE_SVCCTX, C->srv, 0, OCI_ATTR_SERVER, C->err);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                ORAERROR(C);
        C->lastError = OCIHandleAlloc(C->env, (void**)&C->usr, OCI_HTYPE_SESSION, 0, NULL);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                ORAERROR(C);
        C->lastError = OCIAttrSet(C->usr, OCI_HTYPE_SESSION, (dvoid *)username, (int)strlen(username), OCI_ATTR_USERNAME, C->err);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                ORAERROR(C);
        C->lastError = OCIAttrSet(C->usr, OCI_HTYPE_SESSION, (dvoid *)password, (int)strlen(password), OCI_ATTR_PASSWORD, C->err);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                ORAERROR(C);
        C->lastError = OCISessionBegin(C->svc, C->err, C->usr, OCI_CRED_RDBMS, OCI_DEFAULT);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                ORAERROR(C);
        OCIAttrSet(C->svc, OCI_HTYPE_SVCCTX, C->usr, 0, OCI_ATTR_SESSION, C->err);
        return true;
}


/* ----------------------------------------------------- Protected methods */


#ifdef PACKAGE_PROTECTED
#pragma GCC visibility push(hidden)
#endif

T OracleConnection_new(URL_T url, char **error) {
        T C;
        assert(url);
        assert(error);
        NEW(C);
        C->url = url;
        C->sb = StringBuffer_create(STRLEN);
        C->timeout = SQL_DEFAULT_TIMEOUT;
        if (! _doConnect(C, url, error)) {
                OracleConnection_free(&C);
                return NULL;
        }
        C->txnhp = NULL;
        return C;
}


void OracleConnection_free(T* C) {
        assert(C && *C);
        if ((*C)->svc)
                OCISessionEnd((*C)->svc, (*C)->err, (*C)->usr, OCI_DEFAULT);
        if ((*C)->srv)
                OCIServerDetach((*C)->srv, (*C)->err, OCI_DEFAULT);
        if ((*C)->env)
                OCIHandleFree((*C)->env, OCI_HTYPE_ENV);
        StringBuffer_free(&(*C)->sb);
        FREE(*C);
}


void OracleConnection_setQueryTimeout(T C, int ms) {
        assert(C);
        C->timeout = ms;
}


void OracleConnection_setMaxRows(T C, int max) {
        assert(C);
        C->maxRows = max;
}


int  OracleConnection_ping(T C) {
        assert(C);
        C->lastError = OCIPing(C->svc, C->err, OCI_DEFAULT);
        return (C->lastError == OCI_SUCCESS);
}


int  OracleConnection_beginTransaction(T C) {
        assert(C);
        if (C->txnhp == NULL) /* Allocate handler only once, if it is necessary */
        {
            /* allocate transaction handle and set it in the service handle */
            C->lastError = OCIHandleAlloc(C->env, (void **)&C->txnhp, OCI_HTYPE_TRANS, 0, 0);
            if (C->lastError != OCI_SUCCESS) 
                return false;
            OCIAttrSet(C->svc, OCI_HTYPE_SVCCTX, (void *)C->txnhp, 0, OCI_ATTR_TRANS, C->err);
        }
        C->lastError = OCITransStart (C->svc, C->err, ORACLE_TRANSACTION_PERIOD, OCI_TRANS_NEW);
        return (C->lastError == OCI_SUCCESS);
}


int  OracleConnection_commit(T C) {
        assert(C);
        C->lastError = OCITransCommit(C->svc, C->err, OCI_DEFAULT);
        return C->lastError == OCI_SUCCESS;
}


int  OracleConnection_rollback(T C) {
        assert(C);
        C->lastError = OCITransRollback(C->svc, C->err, OCI_DEFAULT);
        return C->lastError == OCI_SUCCESS;
}


long long OracleConnection_lastRowId(T C) {
        /*:FIXME:*/
        /*
         Oracle's RowID can be mapped on string only
         so, currently I leave it unimplemented
         */
        
        /*     OCIRowid* rowid; */
        /*     OCIDescriptorAlloc((dvoid *)C->env,  */
        /*                        (dvoid **)&rowid, */
        /*                        (ub4) OCI_DTYPE_ROWID,  */
        /*                        (size_t) 0, (dvoid **) 0); */
        
        /*     if (OCIAttrGet (select_p, */
        /*                     OCI_HTYPE_STMT, */
        /*                     &rowid,              /\* get the current rowid *\/ */
        /*                     0, */
        /*                     OCI_ATTR_ROWID, */
        /*                     errhp)) */
        /*     { */
        /*         printf ("Getting the Rowid failed \n"); */
        /*         return (OCI_ERROR); */
        /*     } */
        
        /*     OCIDescriptorFree(rowid, OCI_DTYPE_ROWID); */
        DEBUG("OracleConnection_lastRowId: Not implemented yet");
        return -1;
}


long long OracleConnection_rowsChanged(T C) {
        assert(C);
        return C->rowsChanged;
}


int  OracleConnection_execute(T C, const char *sql, va_list ap) {
        OCIStmt* stmtp;
        va_list ap_copy;
        assert(C);
        C->rowsChanged = 0;
        va_copy(ap_copy, ap);
        StringBuffer_vset(C->sb, sql, ap_copy);
        va_end(ap_copy);
        StringBuffer_trim(C->sb);
        /* Build statement */
        C->lastError = OCIHandleAlloc(C->env, (void **)&stmtp, OCI_HTYPE_STMT, 0, NULL);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                return false;
        C->lastError = OCIStmtPrepare(stmtp, C->err, StringBuffer_toString(C->sb), StringBuffer_length(C->sb), OCI_NTV_SYNTAX, OCI_DEFAULT);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO) {
                OCIHandleFree(stmtp, OCI_HTYPE_STMT);
                return false;
        }
        /* Execute */
        C->lastError = OCIStmtExecute(C->svc, stmtp, C->err, 1, 0, NULL, NULL, OCI_DEFAULT);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO) {
                ub4 parmcnt = 0;
                OCIAttrGet(stmtp, OCI_HTYPE_STMT, &parmcnt, NULL, OCI_ATTR_PARSE_ERROR_OFFSET, C->err);
                DEBUG("Error occured in StmtExecute %d (%s), offset is %d\n", C->lastError, OracleConnection_getLastError(C), parmcnt);
                OCIHandleFree(stmtp, OCI_HTYPE_STMT);
                return false;
        }
        C->lastError = OCIAttrGet(stmtp, OCI_HTYPE_STMT, &C->rowsChanged, 0, OCI_ATTR_ROW_COUNT, C->err);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                DEBUG("OracleConnection_execute: Error in OCIAttrGet %d (%s)\n", C->lastError, OracleConnection_getLastError(C));
        OCIHandleFree(stmtp, OCI_HTYPE_STMT);
        return C->lastError == OCI_SUCCESS;
}


ResultSet_T OracleConnection_executeQuery(T C, const char *sql, va_list ap) {
        OCIStmt* stmtp;
        va_list  ap_copy;
        assert(C);
        C->rowsChanged = 0;
        va_copy(ap_copy, ap);
        StringBuffer_vset(C->sb, sql, ap_copy);
        va_end(ap_copy);
        StringBuffer_trim(C->sb);
        /* Build statement */
        C->lastError = OCIHandleAlloc(C->env, (void **)&stmtp, OCI_HTYPE_STMT, 0, NULL);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                return NULL;
        C->lastError = OCIStmtPrepare(stmtp, C->err, StringBuffer_toString(C->sb), StringBuffer_length(C->sb), OCI_NTV_SYNTAX, OCI_DEFAULT);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO) {
                OCIHandleFree(stmtp, OCI_HTYPE_STMT);
                return NULL;
        }
        /* Execute and create Result Set */
        C->lastError = OCIStmtExecute(C->svc, stmtp, C->err, 0, 0, NULL, NULL, OCI_DEFAULT);    
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO) {
                ub4 parmcnt = 0;
                OCIAttrGet(stmtp, OCI_HTYPE_STMT, &parmcnt, NULL, OCI_ATTR_PARSE_ERROR_OFFSET, C->err);
                DEBUG("Error occured in StmtExecute %d (%s), offset is %d\n", C->lastError, OracleConnection_getLastError(C), parmcnt);
                OCIHandleFree(stmtp, OCI_HTYPE_STMT);
                return NULL;
        }
        C->lastError = OCIAttrGet(stmtp, OCI_HTYPE_STMT, &C->rowsChanged, 0, OCI_ATTR_ROW_COUNT, C->err);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                DEBUG("OracleConnection_execute: Error in OCIAttrGet %d (%s)\n", C->lastError, OracleConnection_getLastError(C));
        return ResultSet_new(OracleResultSet_new(stmtp, C->env, C->usr, C->err, C->svc, true, C->maxRows), (Rop_T)&oraclerops);
}


PreparedStatement_T OracleConnection_prepareStatement(T C, const char *sql, va_list ap) {
        OCIStmt *stmtp;
        va_list ap_copy;
        assert(C);
        va_copy(ap_copy, ap);
        StringBuffer_vset(C->sb, sql, ap_copy);
        va_end(ap_copy);
        StringBuffer_trim(C->sb);
        int paramCount = StringBuffer_prepare4oracle(C->sb);
        /* Build statement */
        C->lastError = OCIHandleAlloc(C->env, (void **)&stmtp, OCI_HTYPE_STMT, 0, 0);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO)
                return NULL;
        C->lastError = OCIStmtPrepare(stmtp, C->err, StringBuffer_toString(C->sb), StringBuffer_length(C->sb), OCI_NTV_SYNTAX, OCI_DEFAULT);
        if (C->lastError != OCI_SUCCESS && C->lastError != OCI_SUCCESS_WITH_INFO) {
                OCIHandleFree(stmtp, OCI_HTYPE_STMT);
                return NULL;
        }
        return PreparedStatement_new(OraclePreparedStatement_new(stmtp, C->env, C->usr, C->err, C->svc, C->maxRows), (Pop_T)&oraclepops, paramCount);
}


const char *OracleConnection_getLastError(T C) {
        sb4 errcode;
        switch (C->lastError)
        {
                case OCI_SUCCESS:
                        return "";
                case OCI_SUCCESS_WITH_INFO:
                        return "Error - OCI_SUCCESS_WITH_INFO";
                        break;
                case OCI_NEED_DATA:
                        return "Error - OCI_NEED_DATA";
                        break;
                case OCI_NO_DATA:
                        return "Error - OCI_NODATA";
                        break;
                case OCI_ERROR:
                        (void) OCIErrorGet(C->err, 1, NULL, &errcode, C->erb, (ub4)ERB_SIZE, OCI_HTYPE_ERROR);
                        return C->erb;
                        break;
                case OCI_INVALID_HANDLE:
                        return "Error - OCI_INVALID_HANDLE";
                        break;
                case OCI_STILL_EXECUTING:
                        return "Error - OCI_STILL_EXECUTE";
                        break;
                case OCI_CONTINUE:
                        return "Error - OCI_CONTINUE";
                        break;
                default:
                        break;
        }
        return C->erb;
}


#ifdef PACKAGE_PROTECTED
#pragma GCC visibility pop
#endif
