/******************************************************************************
 *
 * Project:  OpenGIS Simple Features Reference Implementation
 * Purpose:  SQLite REGEXP function
 * Author:   Even Rouault, even dot rouault at spatialys.com
 *
 ******************************************************************************
 * Copyright (c) 2012, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

/* WARNING: VERY IMPORTANT NOTE: This file MUST not be directly compiled as */
/* a standalone object. It must be included from ogrsqlitevirtualogr.cpp */
/* (actually from ogrsqlitesqlfunctions.cpp) */
#ifndef COMPILATION_ALLOWED
#error See comment in file
#endif

/* This code originates from pcre.c from the sqlite3-pcre extension */
/* from http://laltromondo.dynalias.net/~iki/informatica/soft/sqlite3-pcre/ */
/* whose header is : */
/*
 * Written by Alexey Tourbin <at@altlinux.org>.
 *
 * The author has dedicated the code to the public domain.  Anyone is free
 * to copy, modify, publish, use, compile, sell, or distribute the original
 * code, either in source code form or as a compiled binary, for any purpose,
 * commercial or non-commercial, and by any means.
 */

// The pcre2 variant has been ported from
// https://github.com/pfmoore/sqlite-pcre2/blob/main/src/pcre.c which has the
// same license as above.

#include "ogrsqliteregexp.h"
#include "sqlite3.h"

#ifdef HAVE_PCRE2

#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>

typedef struct
{
    char *s;
    pcre2_code *p;
} cache_entry;

constexpr int CACHE_SIZE = 16;

static pcre2_code *re_compile_with_cache(sqlite3_context *ctx, const char *re)
{
    cache_entry *cache = static_cast<cache_entry *>(sqlite3_user_data(ctx));

    CPLAssert(cache);

    bool found = false;
    int i;
    for (i = 0; i < CACHE_SIZE && cache[i].s; i++)
        if (strcmp(re, cache[i].s) == 0)
        {
            found = true;
            break;
        }

    if (found)
    {
        if (i > 0)
        {
            /* Get the found entry */
            cache_entry c = cache[i];
            /* Move 0..i-1 up one - args are (dest, src, size) */
            memmove(cache + 1, cache, i * sizeof(cache_entry));
            /* Put the found entry at the start */
            cache[0] = c;
        }
    }
    else
    {
        /* Create a new entry */
        int errorcode = 0;
        PCRE2_SIZE pos = 0;
        uint32_t has_jit = 0;
        PCRE2_UCHAR8 err_buff[256];

#ifdef HAVE_GCC_DIAGNOSTIC_PUSH
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
#endif
        pcre2_code *pat =
            pcre2_compile(reinterpret_cast<const PCRE2_UCHAR8 *>(re),
                          PCRE2_ZERO_TERMINATED, 0, &errorcode, &pos, nullptr);
#ifdef HAVE_GCC_DIAGNOSTIC_PUSH
#pragma GCC diagnostic pop
#endif
        if (!pat)
        {
            pcre2_get_error_message(errorcode, err_buff, sizeof(err_buff));
            char *e2 = sqlite3_mprintf("%s: %s (offset %d)", re, err_buff, pos);
            sqlite3_result_error(ctx, e2, -1);
            sqlite3_free(e2);
            return nullptr;
        }
        pcre2_config(PCRE2_CONFIG_JIT, &has_jit);
        if (has_jit)
        {
            errorcode = pcre2_jit_compile(pat, 0);
            if (errorcode)
            {
                pcre2_get_error_message(errorcode, err_buff, sizeof(err_buff));
                char *e2 = sqlite3_mprintf("%s: %s", re, err_buff);
                sqlite3_result_error(ctx, e2, -1);
                sqlite3_free(e2);
                pcre2_code_free(pat);
                return nullptr;
            }
        }
        /* Free the last cache entry if necessary */
        i = CACHE_SIZE - 1;
        if (cache[i].s)
        {
            VSIFree(cache[i].s);
            CPLAssert(cache[i].p);
            pcre2_code_free(cache[i].p);
        }
        /* Move everything up to make space */
        memmove(cache + 1, cache, i * sizeof(cache_entry));
        cache[0].s = VSIStrdup(re);
        cache[0].p = pat;
    }

    return cache[0].p;
}

/************************************************************************/
/*                         OGRSQLiteREGEXPFunction()                    */
/************************************************************************/

static void OGRSQLiteREGEXPFunction(sqlite3_context *ctx, CPL_UNUSED int argc,
                                    sqlite3_value **argv)
{
    CPLAssert(argc == 2);

    const char *re =
        reinterpret_cast<const char *>(sqlite3_value_text(argv[0]));
    if (!re)
    {
        sqlite3_result_error(ctx, "no regexp", -1);
        return;
    }

    if (sqlite3_value_type(argv[1]) == SQLITE_NULL)
    {
        sqlite3_result_int(ctx, 0);
        return;
    }

    const char *str =
        reinterpret_cast<const char *>(sqlite3_value_text(argv[1]));
    if (!str)
    {
        sqlite3_result_error(ctx, "no string", -1);
        return;
    }

    pcre2_code *p = re_compile_with_cache(ctx, re);
    if (!p)
        return;

    pcre2_match_data *md = pcre2_match_data_create_from_pattern(p, nullptr);
    if (!md)
    {
        sqlite3_result_error(ctx, "could not create match data block", -1);
        return;
    }

#ifdef HAVE_GCC_DIAGNOSTIC_PUSH
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
#endif
    int rc = pcre2_match(p, reinterpret_cast<const PCRE2_UCHAR8 *>(str),
                         PCRE2_ZERO_TERMINATED, 0, 0, md, nullptr);
#ifdef HAVE_GCC_DIAGNOSTIC_PUSH
#pragma GCC diagnostic pop
#endif

    pcre2_match_data_free(md);

    sqlite3_result_int(ctx, rc >= 0);
}

#elif defined(HAVE_PCRE)

#include <pcre.h>

typedef struct
{
    char *s;
    pcre *p;
    pcre_extra *e;
} cache_entry;

constexpr int CACHE_SIZE = 16;

/************************************************************************/
/*                         OGRSQLiteREGEXPFunction()                    */
/************************************************************************/

static void OGRSQLiteREGEXPFunction(sqlite3_context *ctx, CPL_UNUSED int argc,
                                    sqlite3_value **argv)
{
    CPLAssert(argc == 2);

    const char *re =
        reinterpret_cast<const char *>(sqlite3_value_text(argv[0]));
    if (!re)
    {
        sqlite3_result_error(ctx, "no regexp", -1);
        return;
    }

    if (sqlite3_value_type(argv[1]) == SQLITE_NULL)
    {
        sqlite3_result_int(ctx, 0);
        return;
    }

    const char *str =
        reinterpret_cast<const char *>(sqlite3_value_text(argv[1]));
    if (!str)
    {
        sqlite3_result_error(ctx, "no string", -1);
        return;
    }

    /* simple LRU cache */
    cache_entry *cache = static_cast<cache_entry *>(sqlite3_user_data(ctx));
    CPLAssert(cache);

    bool found = false;
    int i = 0;  // Used after for.
    for (; i < CACHE_SIZE && cache[i].s; i++)
    {
        if (strcmp(re, cache[i].s) == 0)
        {
            found = true;
            break;
        }
    }

    if (found)
    {
        if (i > 0)
        {
            cache_entry c = cache[i];
            memmove(cache + 1, cache, i * sizeof(cache_entry));
            cache[0] = c;
        }
    }
    else
    {
        cache_entry c;
        const char *err = nullptr;
        int pos = 0;
        c.p = pcre_compile(re, 0, &err, &pos, nullptr);
        if (!c.p)
        {
            char *e2 = sqlite3_mprintf("%s: %s (offset %d)", re, err, pos);
            sqlite3_result_error(ctx, e2, -1);
            sqlite3_free(e2);
            return;
        }
        c.e = pcre_study(c.p, 0, &err);
        c.s = VSIStrdup(re);
        if (!c.s)
        {
            sqlite3_result_error(ctx, "strdup: ENOMEM", -1);
            pcre_free(c.p);
            pcre_free(c.e);
            return;
        }
        i = CACHE_SIZE - 1;
        if (cache[i].s)
        {
            CPLFree(cache[i].s);
            CPLAssert(cache[i].p);
            pcre_free(cache[i].p);
            pcre_free(cache[i].e);
        }
        memmove(cache + 1, cache, i * sizeof(cache_entry));
        cache[0] = c;
    }
    pcre *p = cache[0].p;
    CPLAssert(p);
    pcre_extra *e = cache[0].e;

    int rc =
        pcre_exec(p, e, str, static_cast<int>(strlen(str)), 0, 0, nullptr, 0);
    sqlite3_result_int(ctx, rc >= 0);
}

#endif  // HAVE_PCRE

/************************************************************************/
/*                        OGRSQLiteRegisterRegExpFunction()             */
/************************************************************************/

static void *OGRSQLiteRegisterRegExpFunction(sqlite3 *
#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)
                                                 hDB
#endif
)
{
#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)

    /* For debugging purposes mostly */
    if (!CPLTestBool(CPLGetConfigOption("OGR_SQLITE_REGEXP", "YES")))
        return nullptr;

    /* Check if we really need to define our own REGEXP function */
    int rc =
        sqlite3_exec(hDB, "SELECT 'a' REGEXP 'a'", nullptr, nullptr, nullptr);
    if (rc == SQLITE_OK)
    {
        CPLDebug("SQLITE", "REGEXP already available");
        return nullptr;
    }

    cache_entry *cache =
        static_cast<cache_entry *>(CPLCalloc(CACHE_SIZE, sizeof(cache_entry)));
    sqlite3_create_function(hDB, "REGEXP", 2, SQLITE_UTF8, cache,
                            OGRSQLiteREGEXPFunction, nullptr, nullptr);

    /* To clear the error flag */
    sqlite3_exec(hDB, "SELECT 1", nullptr, nullptr, nullptr);

    return cache;
#else   // HAVE_PCRE
    return nullptr;
#endif  // HAVE_PCRE
}

/************************************************************************/
/*                         OGRSQLiteFreeRegExpCache()                   */
/************************************************************************/

static void OGRSQLiteFreeRegExpCache(void *
#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)
                                         hRegExpCache
#endif
)
{
#ifdef HAVE_PCRE2
    if (hRegExpCache == nullptr)
        return;

    cache_entry *cache = static_cast<cache_entry *>(hRegExpCache);
    for (int i = 0; i < CACHE_SIZE && cache[i].s; i++)
    {
        CPLFree(cache[i].s);
        CPLAssert(cache[i].p);
        pcre2_code_free(cache[i].p);
    }
    CPLFree(cache);
#elif defined(HAVE_PCRE)
    if (hRegExpCache == nullptr)
        return;

    cache_entry *cache = static_cast<cache_entry *>(hRegExpCache);
    for (int i = 0; i < CACHE_SIZE && cache[i].s; i++)
    {
        CPLFree(cache[i].s);
        CPLAssert(cache[i].p);
        pcre_free(cache[i].p);
        pcre_free(cache[i].e);
    }
    CPLFree(cache);
#endif  // HAVE_PCRE
}
