/*
 *  Verilog-A math library for Icarus Verilog
 *  http://www.icarus.com/eda/verilog/
 *
 *  Copyright (C) 2007-2021  Cary R. (cygcary@yahoo.com)
 *
 *  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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "vpi_config.h"
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include "vpi_user.h"
#include "ivl_alloc.h"

/*
 * Compile time options: (set in the Makefile.)
 *
 * The functions fmax() and fmin() may not be available in pre-c99
 * libraries, so if they are not available you need to uncomment the
 * -DUSE_MY_FMAX_AND_FMIN in the Makefile.
 *
 * Most of the math functions have been moved to v2005_math. This
 * allows them to be supported separately from the Verilog-A
 * specific functions.
 */


/*
 * These are functionally equivalent fmax() and fmin() implementations
 * that can be used when needed.
 */
#ifndef HAVE_FMIN
static double va_fmin(const double x, const double y)
{
    if (x != x) {return y;}  /* x is NaN so return y. */
    if (y != y) {return x;}  /* y is NaN so return x. */

    return (x < y) ? x : y;
}
#endif
#ifndef HAVE_FMAX
static double va_fmax(const double x, const double y)
{
    if (x != x) {return y;}  /* x is NaN so return y. */
    if (y != y) {return x;}  /* y is NaN so return x. */

    return (x > y) ? x : y;
}
#endif


/* Single argument functions. */
typedef struct s_single_data {
    const char *name;
    double (*func)(double);
} t_single_data;

static t_single_data va_single_data[]= {
    {"$abs",   fabs},
    {0, 0}  /* Must be NULL terminated! */
};


/* Double argument functions. */
typedef struct s_double_data {
    const char *name;
    double (*func)(double, double);
} t_double_data;

static t_double_data va_double_data[]= {
#ifdef HAVE_FMAX
    {"$max",   fmax},
#else
    {"$max",   va_fmax},
#endif
#ifdef HAVE_FMIN
    {"$min",   fmin},
#else
    {"$min",   va_fmin},
#endif
    {0, 0}  /* Must be NULL terminated! */
};


/*
 * This structure holds the single argument information.
 */
typedef struct {
    vpiHandle arg;
    double (*func)(double);
} va_single_t;


/*
 * This structure holds the double argument information.
 */
typedef struct {
    vpiHandle arg1;
    vpiHandle arg2;
    double (*func)(double, double);
} va_double_t;


/*
 * Cleanup the allocated memory at the end of simulation.
 */
static va_single_t** single_funcs = 0;
static unsigned single_funcs_count = 0;
static va_double_t** double_funcs = 0;
static unsigned double_funcs_count = 0;

static PLI_INT32 sys_end_of_simulation(p_cb_data cb_data)
{
    unsigned idx;

    (void)cb_data; /* Parameter is not used. */

    for (idx = 0; idx < single_funcs_count; idx += 1) {
        free(single_funcs[idx]);
    }
    free(single_funcs);
    single_funcs = 0;
    single_funcs_count = 0;

    for (idx = 0; idx < double_funcs_count; idx += 1) {
        free(double_funcs[idx]);
    }
    free(double_funcs);
    double_funcs = 0;
    double_funcs_count = 0;

    return 0;
}


/*
 * Standard error message routine. The format string must take one
 * string argument (the name of the function).
 */
static void va_error_message(vpiHandle callh, const char *format,
                             const char *name) {
    vpi_printf("%s:%d: error: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf(format, name);
    vpip_set_return_value(1);
    vpi_control(vpiFinish, 1);
}


/*
 * Process an argument.
 */
static vpiHandle va_process_argument(vpiHandle callh, const char *name,
                              vpiHandle arg, const char *post) {
    PLI_INT32 type;

    if (arg == NULL) return 0;
    type = vpi_get(vpiType, arg);
    /* Math function cannot do anything with a string. */
    if ((type == vpiConstant || type == vpiParameter) &&
        (vpi_get(vpiConstType, arg) == vpiStringConst)) {
        const char* basemsg = "%s cannot process strings";
        char* msg = malloc(strlen(basemsg)+strlen(post)+3);
        strcpy(msg, basemsg);
        strcat(msg, post);
        strcat(msg, ".\n");
        va_error_message(callh, msg, name);
        free(msg);
        return 0;
    }
    return arg;
}


/*
 * Routine to check all the single argument math functions.
 */
static PLI_INT32 va_single_argument_compiletf(ICARUS_VPI_CONST PLI_BYTE8 *ud)
{
    vpiHandle callh, argv, arg;
    const t_single_data *data;
    const char *name;
    va_single_t* fun_data;

    assert(ud != 0);
    callh = vpi_handle(vpiSysTfCall, 0);
    assert(callh != 0);
    argv = vpi_iterate(vpiArgument, callh);
    data = (const t_single_data *) ud;
    name = data->name;

    fun_data = malloc(sizeof(va_single_t));

    /* Check that there are arguments. */
    if (argv == 0) {
        va_error_message(callh, "%s requires one argument.\n", name);
        free(fun_data);
        return 0;
    }

    /* In Icarus if we have an argv we have at least one argument. */
    arg = vpi_scan(argv);
    fun_data->arg = va_process_argument(callh, name, arg, "");

    /* These functions only take one argument. */
    arg = vpi_scan(argv);
    if (arg != 0) {
        va_error_message(callh, "%s takes only one argument.\n", name);
        vpi_free_object(argv);
    }

    /* Get the function that is to be used by the calltf routine. */
    fun_data->func = data->func;

    vpi_put_userdata(callh, fun_data);
    single_funcs_count += 1;
    single_funcs = (va_single_t **)realloc(single_funcs,
                   single_funcs_count*sizeof(va_single_t **));
    single_funcs[single_funcs_count-1] = fun_data;

    /* vpi_scan() returning 0 (NULL) has already freed argv. */
    return 0;
}


/*
 * Routine to implement the single argument math functions.
 */
static PLI_INT32 va_single_argument_calltf(ICARUS_VPI_CONST PLI_BYTE8 *ud)
{
    vpiHandle callh = vpi_handle(vpiSysTfCall, 0);
    s_vpi_value val;
    va_single_t* fun_data;

    (void)ud; /* Parameter is not used. */

    /* Retrieve the function and argument data. */
    fun_data = vpi_get_userdata(callh);

    /* Calculate the result */
    val.format = vpiRealVal;
    vpi_get_value(fun_data->arg, &val);
    val.value.real = (fun_data->func)(val.value.real);

    /* Return the result */
    vpi_put_value(callh, &val, 0, vpiNoDelay);

    return 0;
}


/*
 * Routine to check all the double argument math functions.
 */
static PLI_INT32 va_double_argument_compiletf(ICARUS_VPI_CONST PLI_BYTE8 *ud)
{
    vpiHandle callh, argv, arg;
    const t_double_data *data;
    const char *name;
    va_double_t* fun_data;

    assert(ud != 0);
    callh = vpi_handle(vpiSysTfCall, 0);
    assert(callh != 0);
    argv = vpi_iterate(vpiArgument, callh);
    data = (const t_double_data *) ud;
    name = data->name;

    fun_data = malloc(sizeof(va_double_t));

    /* Check that there are arguments. */
    if (argv == 0) {
        va_error_message(callh, "%s requires two arguments.\n", name);
        free(fun_data);
        return 0;
    }

    /* In Icarus if we have an argv we have at least one argument. */
    arg = vpi_scan(argv);
    fun_data->arg1 = va_process_argument(callh, name, arg, " (arg1)");

    /* Check that there are at least two arguments. */
    arg = vpi_scan(argv);
    if (arg == 0) {
        va_error_message(callh, "%s requires two arguments.\n", name);
    }
    fun_data->arg2 = va_process_argument(callh, name, arg, " (arg2)");

    /* These functions only take two arguments. */
    arg = vpi_scan(argv);
    if (arg != 0) {
        va_error_message(callh, "%s takes only two arguments.\n", name);
        vpi_free_object(argv);
    }

    /* Get the function that is to be used by the calltf routine. */
    fun_data->func = data->func;

    vpi_put_userdata(callh, fun_data);
    double_funcs_count += 1;
    double_funcs = (va_double_t **)realloc(double_funcs,
                   double_funcs_count*sizeof(va_double_t **));
    double_funcs[double_funcs_count-1] = fun_data;

    /* vpi_scan() returning 0 (NULL) has already freed argv. */
    return 0;
}


/*
 * Routine to implement the double argument math functions.
 */
static PLI_INT32 va_double_argument_calltf(ICARUS_VPI_CONST PLI_BYTE8 *ud)
{
    vpiHandle callh = vpi_handle(vpiSysTfCall, 0);
    s_vpi_value val;
    double first_arg;
    va_double_t* fun_data;

    (void)ud; /* Parameter is not used. */

    /* Retrieve the function and argument data. */
    fun_data = vpi_get_userdata(callh);

    /* Calculate the result */
    val.format = vpiRealVal;
    vpi_get_value(fun_data->arg1, &val);
    first_arg = val.value.real;
    vpi_get_value(fun_data->arg2, &val);
    val.value.real = (fun_data->func)(first_arg, val.value.real);

    /* Return the result */
    vpi_put_value(callh, &val, 0, vpiNoDelay);

    return 0;
}


/*
 * Register all the functions with Verilog.
 */
static void va_math_register(void)
{
    s_cb_data cb_data;
    s_vpi_systf_data tf_data;
    vpiHandle res;
    unsigned idx;

    /* Register the single argument functions. */
    tf_data.type        = vpiSysFunc;
    tf_data.sysfunctype = vpiRealFunc;
    tf_data.calltf      = va_single_argument_calltf;
    tf_data.compiletf   = va_single_argument_compiletf;
    tf_data.sizetf      = 0;

    for (idx=0; va_single_data[idx].name != 0; idx++) {
        tf_data.tfname    = va_single_data[idx].name;
        tf_data.user_data = (PLI_BYTE8 *) &va_single_data[idx];
        res = vpi_register_systf(&tf_data);
        vpip_make_systf_system_defined(res);
    }

    /* Register the double argument functions. */
    tf_data.type        = vpiSysFunc;
    tf_data.sysfunctype = vpiRealFunc;
    tf_data.calltf      = va_double_argument_calltf;
    tf_data.compiletf   = va_double_argument_compiletf;
    tf_data.sizetf      = 0;

    for (idx=0; va_double_data[idx].name != 0; idx++) {
        tf_data.tfname    = va_double_data[idx].name;
        tf_data.user_data = (PLI_BYTE8 *) &va_double_data[idx];
        res = vpi_register_systf(&tf_data);
        vpip_make_systf_system_defined(res);
    }

    /* We need to clean up the userdata. */
    cb_data.reason = cbEndOfSimulation;
    cb_data.time = 0;
    cb_data.cb_rtn = sys_end_of_simulation;
    cb_data.user_data = "system";
    vpi_register_cb(&cb_data);
}


/*
 * Hook to get Icarus Verilog to find the registration function.
 */
void (*vlog_startup_routines[])(void) = {
    va_math_register,
    0
};
