/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
#include <log4cxx/db/dbappender.h>
#include <log4cxx/appenderskeleton.h>
#include <log4cxx/helpers/stringhelper.h>
#include <log4cxx/helpers/pool.h>
#include <log4cxx/helpers/loglog.h>
#include <log4cxx/patternlayout.h>
#include <log4cxx/helpers/transcoder.h>
#include <log4cxx/pattern/loggerpatternconverter.h>
#include <log4cxx/pattern/classnamepatternconverter.h>
#include <log4cxx/pattern/datepatternconverter.h>
#include <log4cxx/pattern/filelocationpatternconverter.h>
#include <log4cxx/pattern/fulllocationpatternconverter.h>
#include <log4cxx/pattern/shortfilelocationpatternconverter.h>
#include <log4cxx/pattern/linelocationpatternconverter.h>
#include <log4cxx/pattern/messagepatternconverter.h>
#include <log4cxx/pattern/methodlocationpatternconverter.h>
#include <log4cxx/pattern/levelpatternconverter.h>
#include <log4cxx/pattern/threadpatternconverter.h>
#include <log4cxx/pattern/threadusernamepatternconverter.h>
#include <log4cxx/pattern/ndcpatternconverter.h>
#include <log4cxx/private/appenderskeleton_priv.h>
#include <apr_dbd.h>
#include <assert.h>

using namespace LOG4CXX_NS;
using namespace LOG4CXX_NS::helpers;
using namespace LOG4CXX_NS::db;
using namespace LOG4CXX_NS::spi;
using namespace LOG4CXX_NS::pattern;

IMPLEMENT_LOG4CXX_OBJECT(DBAppender)

#define _priv static_cast<DBAppenderPriv*>(m_priv.get())

struct DBAppender::DBAppenderPriv : public AppenderSkeleton::AppenderSkeletonPrivate
{
    DBAppenderPriv() :
        AppenderSkeletonPrivate()
	{
		static bool initialized = false;
		if (!initialized)
		{
			initialized = true;
			apr_status_t stat = apr_dbd_init(m_pool.getAPRPool());
			assert(stat == APR_SUCCESS);
		}
	}

    const apr_dbd_driver_t* m_driver = nullptr;
    apr_dbd_t* m_databaseHandle = nullptr;
    apr_dbd_prepared_t* preparedStmt = nullptr;
    std::vector<LogString> mappedName;
    std::string driverName;
    std::string driverParams;
    std::string databaseName;
    std::string sqlStatement;
    Pool m_pool;
    std::vector<pattern::LoggingEventPatternConverterPtr> converters;

    void close();
};

#define RULES_PUT(spec, cls) \
    specs.insert(PatternMap::value_type(LogString(LOG4CXX_STR(spec)), cls ::newInstance))

static PatternMap getFormatSpecifiers()
{
    PatternMap specs;
    if (specs.empty())
    {
        RULES_PUT("logger", LoggerPatternConverter);
        RULES_PUT("class", ClassNamePatternConverter);
        RULES_PUT("time", DatePatternConverter);
        RULES_PUT("shortfilename", ShortFileLocationPatternConverter);
        RULES_PUT("fullfilename", FileLocationPatternConverter);
        RULES_PUT("location", FullLocationPatternConverter);
        RULES_PUT("line", LineLocationPatternConverter);
        RULES_PUT("message", MessagePatternConverter);
        RULES_PUT("method", MethodLocationPatternConverter);
        RULES_PUT("level", LevelPatternConverter);
        RULES_PUT("thread", ThreadPatternConverter);
        RULES_PUT("threadname", ThreadUsernamePatternConverter);
        RULES_PUT("ndc", NDCPatternConverter);
    }
    return specs;
}

DBAppender::DBAppender()
    : AppenderSkeleton (std::make_unique<DBAppenderPriv>())
{
}

DBAppender::~DBAppender()
{
    if (_priv->setClosed())
        _priv->close();
}

void DBAppender::close()
{
    if (_priv->setClosed())
        _priv->close();
}

void DBAppender::DBAppenderPriv::close()
{
    if(this->m_driver && this->m_databaseHandle){
        apr_dbd_close(this->m_driver, this->m_databaseHandle);
    }
    this->m_driver = nullptr;
    this->m_databaseHandle = nullptr;
}

void DBAppender::setOption(const LogString& option, const LogString& value){
    if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("COLUMNMAPPING"), LOG4CXX_STR("columnmapping")))
    {
        _priv->mappedName.push_back(value);
    }
    else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("DRIVERNAME"), LOG4CXX_STR("drivername")))
    {
        Transcoder::encodeUTF8(value, _priv->driverName);
    }
    else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("DRIVERPARAMS"), LOG4CXX_STR("driverparams")))
    {
        Transcoder::encodeUTF8(value, _priv->driverParams);
    }
    else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("DATABASENAME"), LOG4CXX_STR("databasename")))
    {
        Transcoder::encodeUTF8(value, _priv->databaseName);
    }
    else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("SQL"), LOG4CXX_STR("sql")))
    {
        Transcoder::encodeUTF8(value, _priv->sqlStatement);
    }
    else
    {
        AppenderSkeleton::setOption(option, value);
    }
}

void DBAppender::activateOptions( LOG4CXX_ACTIVATE_OPTIONS_FORMAL_PARAMETERS )
{
    apr_status_t stat = apr_dbd_get_driver(_priv->m_pool.getAPRPool(),
                                           _priv->driverName.c_str(),
#if 15 < LOG4CXX_ABI_VERSION
                                           &_priv->m_driver);
#else
                                           const_cast<const apr_dbd_driver_t**>(&_priv->m_driver));
#endif

    if(stat != APR_SUCCESS){
        LogString errMsg = LOG4CXX_STR("Unable to get driver named ");
        LOG4CXX_DECODE_CHAR(driverName, _priv->driverName);
        errMsg.append(driverName);
        LogLog::error(errMsg);
        _priv->errorHandler->error(errMsg);
        return;
    }

    stat = apr_dbd_open(_priv->m_driver,
                        _priv->m_pool.getAPRPool(),
                        _priv->driverParams.c_str(),
                        &_priv->m_databaseHandle);
    if(stat != APR_SUCCESS){
        LogLog::error(LOG4CXX_STR("Unable to open database"));
        _priv->errorHandler->error(LOG4CXX_STR("Unable to open database"));
        return;
    }

    if(!_priv->databaseName.empty()){
        apr_dbd_set_dbname(_priv->m_driver,
                           _priv->m_pool.getAPRPool(),
                           _priv->m_databaseHandle,
                           _priv->databaseName.c_str());
    }

    stat = apr_dbd_prepare(_priv->m_driver,
                           _priv->m_pool.getAPRPool(),
                           _priv->m_databaseHandle,
                           _priv->sqlStatement.c_str(),
                           "log_insert",
                           &_priv->preparedStmt);
    if(stat != APR_SUCCESS){
        LogString error = LOG4CXX_STR("Unable to prepare statement: ");
        std::string dbdErr(apr_dbd_error(_priv->m_driver, _priv->m_databaseHandle, stat));
        LOG4CXX_DECODE_CHAR(dbdErrLS, dbdErr);
        error.append(dbdErrLS);
        LogLog::error(error);
        _priv->errorHandler->error(error);
        return;
    }

    auto specs = getFormatSpecifiers();
    for (auto& name : _priv->mappedName)
    {
        auto pItem = specs.find(StringHelper::toLowerCase(name));
        if (specs.end() == pItem)
            LogLog::error(name + LOG4CXX_STR(" is not a supported ColumnMapping value"));
        else
        {
            std::vector<LogString> options;
            if (LOG4CXX_STR("time") == pItem->first)
                options.push_back(LOG4CXX_STR("yyyy-MM-ddTHH:mm:ss.SSS"));
            pattern::LoggingEventPatternConverterPtr converter = LOG4CXX_NS::cast<LoggingEventPatternConverter>((pItem->second)(options));
            _priv->converters.push_back(converter);
        }
    }
}

void DBAppender::append( LOG4CXX_APPEND_FORMAL_PARAMETERS ){
	std::vector<std::string> ls_args;
    std::vector<const char*> args;
    int stat;
    int num_rows;

    if(_priv->m_driver == nullptr ||
            _priv->m_databaseHandle == nullptr ||
            _priv->preparedStmt == nullptr){
        _priv->errorHandler->error(LOG4CXX_STR("DBAppender not initialized properly: logging not available"));
        return;
    }

    for(auto& converter : _priv->converters){
		LogString str_data;
		converter->format(event, str_data);
		LOG4CXX_ENCODE_CHAR(new_str_data, str_data);
		ls_args.push_back(new_str_data);
    }

	for(std::string& str : ls_args){
        args.push_back(str.data());
    }
    args.push_back(nullptr);

    stat = apr_dbd_pquery(_priv->m_driver,
                          _priv->m_pool.getAPRPool(),
                          _priv->m_databaseHandle,
                          &num_rows,
                          _priv->preparedStmt,
                          int(args.size()),
                          args.data());
    if(stat != APR_SUCCESS){
        LogString error = LOG4CXX_STR("Unable to insert: ");
		LOG4CXX_DECODE_CHAR(local_error, apr_dbd_error(_priv->m_driver, _priv->m_databaseHandle, stat));
		error.append(local_error);
        LogLog::error(error);
        _priv->errorHandler->error(error);
    }
}
