/* Copyright (C) 2007-2013 Open Information Security Foundation
 *
 * You can copy, redistribute or modify this Program under the terms of
 * the GNU General Public License version 2 as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * version 2 along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * \file
 *
 * \author Endace Technology Limited.
 * \author Victor Julien <victor@inliniac.net>
 *
 * An API for rule profiling operations.
 */

#include "suricata-common.h"
#include "util-profiling.h"
#include "util-profiling-locks.h"

#ifdef PROFILING
#include "detect-engine.h"
#include "tm-threads.h"
#include "util-conf.h"
#include "util-time.h"

/**
 * Extra data for rule profiling.
 */
typedef struct SCProfileKeywordData_ {
    uint64_t checks;
    uint64_t matches;
    uint64_t max;
    uint64_t ticks_match;
    uint64_t ticks_no_match;
} SCProfileKeywordData;

typedef struct SCProfileKeywordDetectCtx_ {
    uint32_t id;
    SCProfileKeywordData *data;
    pthread_mutex_t data_m;
} SCProfileKeywordDetectCtx;

static int profiling_keywords_output_to_file = 0;
int profiling_keyword_enabled = 0;
thread_local int profiling_keyword_entered = 0;
static char profiling_file_name[PATH_MAX];
static const char *profiling_file_mode = "a";

void SCProfilingKeywordsGlobalInit(void)
{
    ConfNode *conf;

    conf = ConfGetNode("profiling.keywords");
    if (conf != NULL) {
        if (ConfNodeChildValueIsTrue(conf, "enabled")) {
            profiling_keyword_enabled = 1;
            const char *filename = ConfNodeLookupChildValue(conf, "filename");
            if (filename != NULL) {
                const char *log_dir;
                log_dir = ConfigGetLogDirectory();

                snprintf(profiling_file_name, sizeof(profiling_file_name), "%s/%s",
                        log_dir, filename);

                const char *v = ConfNodeLookupChildValue(conf, "append");
                if (v == NULL || ConfValIsTrue(v)) {
                    profiling_file_mode = "a";
                } else {
                    profiling_file_mode = "w";
                }

                profiling_keywords_output_to_file = 1;
            }
        }
    }
}

static void DoDump(SCProfileKeywordDetectCtx *rules_ctx, FILE *fp, const char *name)
{
    int i;
    fprintf(fp, "  ----------------------------------------------"
            "------------------------------------------------------"
            "----------------------------\n");
    fprintf(fp, "  Stats for: %s\n", name);
    fprintf(fp, "  ----------------------------------------------"
            "------------------------------------------------------"
            "----------------------------\n");
    fprintf(fp, "  %-16s %-15s %-15s %-15s %-15s %-15s %-15s %-15s\n", "Keyword", "Ticks", "Checks", "Matches", "Max Ticks", "Avg", "Avg Match", "Avg No Match");
    fprintf(fp, "  ---------------- "
                "--------------- "
                "--------------- "
                "--------------- "
                "--------------- "
                "--------------- "
                "--------------- "
                "--------------- "
        "\n");
    for (i = 0; i < DETECT_TBLSIZE; i++) {
        SCProfileKeywordData *d = &rules_ctx->data[i];
        if (d == NULL || d->checks == 0)
            continue;

        uint64_t ticks = d->ticks_match + d->ticks_no_match;
        double avgticks = 0;
        double avgticks_match = 0;
        double avgticks_no_match = 0;
        if (ticks && d->checks) {
            avgticks = (double)(ticks / d->checks);

            if (d->ticks_match && d->matches)
                avgticks_match = (double)(d->ticks_match / d->matches);
            if (d->ticks_no_match && (d->checks - d->matches) != 0)
                avgticks_no_match = (double)(d->ticks_no_match / (d->checks - d->matches));
        }

        fprintf(fp,
            "  %-16s %-15"PRIu64" %-15"PRIu64" %-15"PRIu64" %-15"PRIu64" %-15.2f %-15.2f %-15.2f\n",
            sigmatch_table[i].name,
            ticks,
            d->checks,
            d->matches,
            d->max,
            avgticks,
            avgticks_match,
            avgticks_no_match);
    }
}

static void
SCProfilingKeywordDump(DetectEngineCtx *de_ctx)
{
    int i;
    FILE *fp;
    struct timeval tval;
    struct tm *tms;
    struct tm local_tm;

    if (profiling_keyword_enabled == 0)
        return;

    const int nlists = de_ctx->buffer_type_id;
    gettimeofday(&tval, NULL);
    tms = SCLocalTime(tval.tv_sec, &local_tm);

    if (profiling_keywords_output_to_file == 1) {
        SCLogDebug("file %s mode %s", profiling_file_name, profiling_file_mode);

        fp = fopen(profiling_file_name, profiling_file_mode);

        if (fp == NULL) {
            SCLogError("failed to open %s: %s", profiling_file_name, strerror(errno));
            return;
        }
    } else {
       fp = stdout;
    }

    fprintf(fp, "  ----------------------------------------------"
            "------------------------------------------------------"
            "----------------------------\n");
    fprintf(fp, "  Date: %" PRId32 "/%" PRId32 "/%04d -- "
            "%02d:%02d:%02d\n", tms->tm_mon + 1, tms->tm_mday, tms->tm_year + 1900,
            tms->tm_hour,tms->tm_min, tms->tm_sec);

    /* global stats first */
    DoDump(de_ctx->profile_keyword_ctx, fp, "total");
    /* per buffer stats next, but only if there are stats to print */
    for (i = 0; i < nlists; i++) {
        int j;
        uint64_t checks = 0;
        for (j = 0; j < DETECT_TBLSIZE; j++) {
            checks += de_ctx->profile_keyword_ctx_per_list[i]->data[j].checks;
        }

        if (checks) {
            const char *name = NULL;
            if (i < DETECT_SM_LIST_DYNAMIC_START) {
                name = DetectSigmatchListEnumToString(i);
            } else {
                name = DetectEngineBufferTypeGetNameById(de_ctx, i);
            }

            DoDump(de_ctx->profile_keyword_ctx_per_list[i], fp, name);
        }
    }

    fprintf(fp,"\n");
    if (fp != stdout)
        fclose(fp);

    SCLogPerf("Done dumping keyword profiling data.");
}

/**
 * \brief Update a rule counter.
 *
 * \param id The ID of this counter.
 * \param ticks Number of CPU ticks for this rule.
 * \param match Did the rule match?
 */
void
SCProfilingKeywordUpdateCounter(DetectEngineThreadCtx *det_ctx, int id, uint64_t ticks, int match)
{
    if (det_ctx != NULL && det_ctx->keyword_perf_data != NULL && id < DETECT_TBLSIZE) {
        SCProfileKeywordData *p = &det_ctx->keyword_perf_data[id];

        p->checks++;
        p->matches += match;
        if (ticks > p->max)
            p->max = ticks;
        if (match == 1)
            p->ticks_match += ticks;
        else
            p->ticks_no_match += ticks;

        /* store per list (buffer type) as well */
        if (det_ctx->keyword_perf_list >= 0) {// && det_ctx->keyword_perf_list < DETECT_SM_LIST_MAX) {
            p = &det_ctx->keyword_perf_data_per_list[det_ctx->keyword_perf_list][id];
            p->checks++;
            p->matches += match;
            if (ticks > p->max)
                p->max = ticks;
            if (match == 1)
                p->ticks_match += ticks;
            else
                p->ticks_no_match += ticks;
        }
    }
}

static SCProfileKeywordDetectCtx *SCProfilingKeywordInitCtx(void)
{
    SCProfileKeywordDetectCtx *ctx = SCCalloc(1, sizeof(SCProfileKeywordDetectCtx));
    if (ctx != NULL) {

        if (pthread_mutex_init(&ctx->data_m, NULL) != 0) {
            FatalError("Failed to initialize hash table mutex.");
        }
    }

    return ctx;
}

static void DetroyCtx(SCProfileKeywordDetectCtx *ctx)
{
    if (ctx) {
        if (ctx->data != NULL)
            SCFree(ctx->data);
        pthread_mutex_destroy(&ctx->data_m);
        SCFree(ctx);
    }
}

void SCProfilingKeywordDestroyCtx(DetectEngineCtx *de_ctx)
{
    if (de_ctx != NULL) {
        SCProfilingKeywordDump(de_ctx);

        DetroyCtx(de_ctx->profile_keyword_ctx);

        const int nlists = de_ctx->buffer_type_id;
        int i;
        for (i = 0; i < nlists; i++) {
            DetroyCtx(de_ctx->profile_keyword_ctx_per_list[i]);
        }
        SCFree(de_ctx->profile_keyword_ctx_per_list);
    }
}

void SCProfilingKeywordThreadSetup(SCProfileKeywordDetectCtx *ctx, DetectEngineThreadCtx *det_ctx)
{
    if (ctx == NULL)
        return;

    SCProfileKeywordData *a = SCCalloc(DETECT_TBLSIZE, sizeof(SCProfileKeywordData));
    if (a != NULL) {
        det_ctx->keyword_perf_data = a;
    }

    const int nlists = det_ctx->de_ctx->buffer_type_id;
    det_ctx->keyword_perf_data_per_list = SCCalloc(nlists, sizeof(SCProfileKeywordData *));
    BUG_ON(det_ctx->keyword_perf_data_per_list == NULL);

    int i;
    for (i = 0; i < nlists; i++) {
        SCProfileKeywordData *b = SCCalloc(DETECT_TBLSIZE, sizeof(SCProfileKeywordData));
        if (b != NULL) {
            det_ctx->keyword_perf_data_per_list[i] = b;
        }
    }
}

static void SCProfilingKeywordThreadMerge(DetectEngineCtx *de_ctx, DetectEngineThreadCtx *det_ctx)
{
    if (de_ctx == NULL || de_ctx->profile_keyword_ctx == NULL ||
        de_ctx->profile_keyword_ctx->data == NULL || det_ctx == NULL ||
        det_ctx->keyword_perf_data == NULL)
        return;

    int i;
    for (i = 0; i < DETECT_TBLSIZE; i++) {
        de_ctx->profile_keyword_ctx->data[i].checks += det_ctx->keyword_perf_data[i].checks;
        de_ctx->profile_keyword_ctx->data[i].matches += det_ctx->keyword_perf_data[i].matches;
        de_ctx->profile_keyword_ctx->data[i].ticks_match += det_ctx->keyword_perf_data[i].ticks_match;
        de_ctx->profile_keyword_ctx->data[i].ticks_no_match += det_ctx->keyword_perf_data[i].ticks_no_match;
        if (det_ctx->keyword_perf_data[i].max > de_ctx->profile_keyword_ctx->data[i].max)
            de_ctx->profile_keyword_ctx->data[i].max = det_ctx->keyword_perf_data[i].max;
    }

    const int nlists = det_ctx->de_ctx->buffer_type_id;
    int j;
    for (j = 0; j < nlists; j++) {
        for (i = 0; i < DETECT_TBLSIZE; i++) {
            de_ctx->profile_keyword_ctx_per_list[j]->data[i].checks += det_ctx->keyword_perf_data_per_list[j][i].checks;
            de_ctx->profile_keyword_ctx_per_list[j]->data[i].matches += det_ctx->keyword_perf_data_per_list[j][i].matches;
            de_ctx->profile_keyword_ctx_per_list[j]->data[i].ticks_match += det_ctx->keyword_perf_data_per_list[j][i].ticks_match;
            de_ctx->profile_keyword_ctx_per_list[j]->data[i].ticks_no_match += det_ctx->keyword_perf_data_per_list[j][i].ticks_no_match;
            if (det_ctx->keyword_perf_data_per_list[j][i].max > de_ctx->profile_keyword_ctx_per_list[j]->data[i].max)
                de_ctx->profile_keyword_ctx_per_list[j]->data[i].max = det_ctx->keyword_perf_data_per_list[j][i].max;
        }
    }
}

void SCProfilingKeywordThreadCleanup(DetectEngineThreadCtx *det_ctx)
{
    if (det_ctx == NULL || det_ctx->de_ctx == NULL || det_ctx->keyword_perf_data == NULL)
        return;

    pthread_mutex_lock(&det_ctx->de_ctx->profile_keyword_ctx->data_m);
    SCProfilingKeywordThreadMerge(det_ctx->de_ctx, det_ctx);
    pthread_mutex_unlock(&det_ctx->de_ctx->profile_keyword_ctx->data_m);

    SCFree(det_ctx->keyword_perf_data);
    det_ctx->keyword_perf_data = NULL;

    const int nlists = det_ctx->de_ctx->buffer_type_id;
    int i;
    for (i = 0; i < nlists; i++) {
        SCFree(det_ctx->keyword_perf_data_per_list[i]);
        det_ctx->keyword_perf_data_per_list[i] = NULL;
    }
    SCFree(det_ctx->keyword_perf_data_per_list);
}

/**
 * \brief Register the keyword profiling counters.
 *
 * \param de_ctx The active DetectEngineCtx, used to get at the loaded rules.
 */
void
SCProfilingKeywordInitCounters(DetectEngineCtx *de_ctx)
{
    if (profiling_keyword_enabled == 0)
        return;

    const int nlists = de_ctx->buffer_type_id;

    de_ctx->profile_keyword_ctx = SCProfilingKeywordInitCtx();
    BUG_ON(de_ctx->profile_keyword_ctx == NULL);

    de_ctx->profile_keyword_ctx->data = SCCalloc(DETECT_TBLSIZE, sizeof(SCProfileKeywordData));
    BUG_ON(de_ctx->profile_keyword_ctx->data == NULL);

    de_ctx->profile_keyword_ctx_per_list = SCCalloc(nlists, sizeof(SCProfileKeywordDetectCtx *));
    BUG_ON(de_ctx->profile_keyword_ctx_per_list == NULL);

    int i;
    for (i = 0; i < nlists; i++) {
        de_ctx->profile_keyword_ctx_per_list[i] = SCProfilingKeywordInitCtx();
        BUG_ON(de_ctx->profile_keyword_ctx_per_list[i] == NULL);
        de_ctx->profile_keyword_ctx_per_list[i]->data =
                SCCalloc(DETECT_TBLSIZE, sizeof(SCProfileKeywordData));
        BUG_ON(de_ctx->profile_keyword_ctx_per_list[i]->data == NULL);
    }

    SCLogPerf("Registered %"PRIu32" keyword profiling counters.", DETECT_TBLSIZE);
}

#endif /* PROFILING */
