/*
 * This file is part of the ESO Pipelines
 * Copyright (C) 2016 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *  Created on: Jul 17, 2020
 *      Author: lluvaul
 */


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*-----------------------------------------------------------------------------
                                   Includes
 -----------------------------------------------------------------------------*/

#include "sbrm.h"
#include <string.h>  // for memcpy & strstr
#include <stdbool.h>
#include <cxmessages.h>

#ifdef NDEBUG
#define ASSERT(msg, x)
#else
#include <assert.h>
#define ASSERT(msg, x) const bool SBRM_##msg = x; assert(SBRM_##msg)
#endif

#define HERE_SIG SBRM_PRIV_HERE_SIG


/*----------------------------------------------------------------------------*/
/**
 * @defgroup sbrm  RAII/SBRM related functions
 *
 * TBD
 */
/*----------------------------------------------------------------------------*/

/**@{*/

static char const * g_catcher = NULL;

/*----------------------------------------------------------------------------*/
static inline
void sbrm_destroy_and_nullify( __sbrm_entry_t * e )
{
    switch (e->fp) {
    case 'w': { void  (*fp)(void**) = e->dtor;       fp(&e->o); break; }
    case 'v': { void  (*fp)(void*)  = e->dtor;       fp(e->o);  break; }
    case 'p': { void* (*fp)(void*)  = e->dtor; (void)fp(e->o);  break; }
    case 'i': { int   (*fp)(void*)  = e->dtor; (void)fp(e->o);  break; }
    case 'f': { float (*fp)(void*)  = e->dtor; (void)fp(e->o);  break; }
    case 'd': { double(*fp)(void*)  = e->dtor; (void)fp(e->o);  break; }
    default: { ASSERT(dtor_return_type_is_well_defined, false); };
    }
    e->o = NULL;  // auto-NULL'd ptrs! :)
}

/*----------------------------------------------------------------------------*/
static
void * sbrm_cleanup( sbrm_registry * r, void * target )
{
#ifndef NDEBUG
    cpl_errorstate state = cpl_errorstate_get();
    bool found = false;
#endif
    void * preserve = NULL;  // points to the special rval (if set & if found)

    // if user has asked to preserve an object for return, mark it as such
    if (target) r->rv = target;  // or: r->rval(r, target);

    // destroy all registered objects except the designated return object
    for (int i = r->avail; i < r->sz; ++i) {  // start from last occupied
        __sbrm_entry_t * e = r->list + i;

        // internal consistency check
        ASSERT(report_this_error_to_technical_staff, e->dtor || !e->o);

        if (e == r->rv) {
            preserve = e->o;
#ifndef NDEBUG
            found = true;
#endif
            continue;
        }

        if (!e->dtor || !e->o) continue;

        sbrm_destroy_and_nullify(e);
    }

    // it is an error to ask to return something that isn't managed:
    // cpl_image * bob = ...; SBRM_CLEANUP(bob);
    ASSERT(only_managed_objects_are_returned, found || !preserve);

    // free the registry itself
    cpl_free(r);

    // interesting parallel to C++: dtor's should not throw exceptions
    ASSERT(dtors_do_not_set_errors, cpl_errorstate_is_equal(state));

    return preserve;
}

/*----------------------------------------------------------------------------*/
static
void sbrm_free( sbrm_registry * r, void * target )
{
    if (!target) return;

    for (int i = r->sz; i--; ) {
        __sbrm_entry_t * e = r->list + i;

        // internal consistency check
        ASSERT(report_this_error_to_technical_staff, e->dtor || !e->o);

        // not the one we want: keep looking
        if (e != target) continue;

        // it is an error to free an unitit'd entry (no dtor or o*)
        // (it'd be unusual to hit this: user doing ptr hijinks?)
        ASSERT(uninitd_registry_slots_are_never_freed, e->dtor);

        // freed earlier or never set: we're done
        if (!e->o) return;

        sbrm_destroy_and_nullify(e);
        return;
    }

    // it is an error to free an unmanaged object, for example:
    // cpl_image * a = ...; SBRM_FREE(a);
    ASSERT(only_managed_objects_are_ever_freed, 0);
}

/*----------------------------------------------------------------------------*/
static
__sbrm_void_wrapper_t sbrm_reset1( sbrm_registry * r, void * target )
{
    r->free(r, target);
    return target;
}

/*----------------------------------------------------------------------------*/
static
void ** sbrm_reset2( sbrm_registry * r, void * target )
{
    r->free(r, target);
    return target;
}

/*----------------------------------------------------------------------------*/
static
void * sbrm_set( sbrm_registry * r, char fp, void * dtor, void * initval,
        const int ident )
{
    if (ident > 0)  // ident given: find, free, and return (or fall thru & add)
        // r->list not guaranteed to be sorted by ident (use of ever-increasing
        // line numbers makes you think so, but flow control branching breaks
        // strict ordering), so use linear search instead of binary, whose per-
        // formance is probably better than binary anyway since the numbers in-
        // volved are so small (10s to 100s of entries)
        for (int i = r->avail; i < r->sz; ++i) {  // search backwards from last
            __sbrm_entry_t * e = r->list + i;
            if (ident != e->ident) continue;
            r->free(r, e);
            return e;
        }

    // need to increase sz in INIT(sz, ...)
    ASSERT(registry_size_can_hold_all_managed_objects, r->avail);

    r->list[--r->avail] = (__sbrm_entry_t){initval, fp, dtor, ident};
    return r->list + r->avail;
}

// this is a verbatim copy of cpl_errorstate_dump_one_level() from
// cpl_errorstate.c but with all cpl_func references changed to g_catcher
/*----------------------------------------------------------------------------*/
static
void sbrm_dump( void (*messenger)(const char *, const char *, ...),
        unsigned self, unsigned first, unsigned last )
{
    const cpl_boolean is_reverse = first > last ? CPL_TRUE : CPL_FALSE;
    const unsigned    newest     = is_reverse ? first : last;
    const unsigned    oldest     = is_reverse ? last : first;
    const char      * revmsg     = is_reverse ? " in reverse order" : "";

    cx_assert( messenger != NULL );
    cx_assert( oldest    <= self );
    cx_assert( newest    >= self );

    if (newest == 0) {
        messenger(g_catcher, "No error(s) to dump");
        cx_assert( oldest == 0);
    } else {
        cx_assert( oldest > 0);
        cx_assert( newest >= oldest);
        if (self == first) {
            if (oldest == 1) {
                messenger(g_catcher, "Dumping all %u error(s)%s:", newest,
                          revmsg);
            } else {
                messenger(g_catcher, "Dumping the %u most recent error(s) out "
                          "of a total of %u errors%s:", newest - oldest + 1,
                          newest, revmsg);
            }
            cpl_msg_indent_more();
        }

        messenger(g_catcher, "[%u/%u] '%s' (%u) at %s", self, newest,
                  cpl_error_get_message(), cpl_error_get_code(),
                  cpl_error_get_where());

        if (self == last) cpl_msg_indent_less();
    }
}

/*----------------------------------------------------------------------------*/
static
void sbrm_dump_error( unsigned self, unsigned first, unsigned last )
{
    sbrm_dump(cpl_msg_error, self, first, last);
}

/*----------------------------------------------------------------------------*/
static
void sbrm_dump_warn( unsigned self, unsigned first, unsigned last )
{
    sbrm_dump(cpl_msg_warning, self, first, last);
}

/*----------------------------------------------------------------------------*/
static
void sbrm_dump_info( unsigned self, unsigned first, unsigned last )
{
    sbrm_dump(cpl_msg_info, self, first, last);
}

/*----------------------------------------------------------------------------*/
static
void sbrm_dump_debug( unsigned self, unsigned first, unsigned last )
{
    sbrm_dump(cpl_msg_debug, self, first, last);
}

/*----------------------------------------------------------------------------*/
static
void * sbrm_dumperr_action( sbrm_registry * r, const bool ret, HERE_SIG,
        void (*lvl)(unsigned, unsigned, unsigned), cpl_error_code code,
        const char * fmt, va_list vargs )
{
    const cpl_error_code zcode = cpl_error_get_code();
    const bool propagate = !code && zcode;

    // first: set new error (supplied or unspecified) or propagate existing
    if (!code) code = zcode ? zcode : CPL_ERROR_UNSPECIFIED;
    const bool extra = fmt && fmt[0] && (fmt[0] != ' ' || fmt[1]);
    char * msg = propagate ? "propagated" : "";
    if (extra) {
        char * details = cpl_vsprintf(fmt, vargs);
        msg = cpl_sprintf("%s%s(%s)", msg, msg[0] ? " " : "", details);
        cpl_free(details);
    }
    cpl_error_set_message_one_macro(func, code, file, line, msg);
    if (extra) cpl_free(msg);

    // second: log/print the error(s) present
    g_catcher = func;
    cpl_errorstate_dump(r->estate, 0, lvl);

    // third: action
    if (ret) return r->cleanup(r, NULL);
    cpl_error_reset();
    r->estate = cpl_errorstate_get();
    return NULL;
}

/*----------------------------------------------------------------------------*/
static
void * sbrm_abort( sbrm_registry * r, HERE_SIG, const unsigned code,
        const char * fmt, ... )
{
    va_list args;
    va_start(args, fmt);
    void * obj = sbrm_dumperr_action(
        r, true, func, file, line, sbrm_dump_error, code, fmt, args);
    va_end(args);
    return obj;
}

/*----------------------------------------------------------------------------*/
static
void sbrm_warn( sbrm_registry * r, HERE_SIG, const char * fmt, ... )
{
    va_list args;
    va_start(args, fmt);
    (void)sbrm_dumperr_action(
        r, false, func, file, line, sbrm_dump_warn, 0, fmt, args);
    va_end(args);
}

/*----------------------------------------------------------------------------*/
static
void sbrm_info( sbrm_registry * r, HERE_SIG, const char * fmt, ... )
{
    va_list args;
    va_start(args, fmt);
    (void)sbrm_dumperr_action(
        r, false, func, file, line, sbrm_dump_info, 0, fmt, args);
    va_end(args);
}

/*----------------------------------------------------------------------------*/
static
void sbrm_debug( sbrm_registry * r, HERE_SIG, const char * fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    (void)sbrm_dumperr_action(
        r, false, func, file, line, sbrm_dump_debug, 0, fmt, args);
    va_end(args);
}

/*----------------------------------------------------------------------------*/
static
void sbrm_rval( sbrm_registry * r, void * target )
{
    r->rv = target;
}

/*----------------------------------------------------------------------------*/
static
void * sbrm_yank( void * target )
{
    __sbrm_entry_t * e = target;
    void * tmp = e->o;
    e->o = NULL;
    return tmp;
}

/*----------------------------------------------------------------------------*/
inline
void * __sbrm_cp( void * dest, void * src, size_t sz, int release )
{
    memcpy(dest, src, sz);
    if (release) cpl_free(src);
    return dest;
}

/*----------------------------------------------------------------------------*/
sbrm_registry * sbrm_init( const int sz, HERE_SIG )
{
    sbrm_registry * reg = SBRM_VALLOC(sbrm_registry, __sbrm_entry_t,
        sz, sz, NULL, cpl_errorstate_get(),
        sbrm_set, sbrm_reset1, sbrm_reset2,
        sbrm_free, sbrm_cleanup,
        sbrm_debug, sbrm_info, sbrm_warn, sbrm_abort,
        sbrm_rval, sbrm_yank
    );

    for (int i = 0; i < sz; ++i)
        reg->list[i] = (__sbrm_entry_t){NULL, 'v', NULL, 0};

    if (cpl_error_get_code()) sbrm_warn(reg, func, file, line,
        "Error present at start of %s!", func);

    return reg;
}

/**@}*/

