/*
 * Copyright - See the COPYRIGHT that is included with this distribution.
 * pvxs is distributed subject to a Software License Agreement found
 * in file LICENSE that is included with this distribution.
 *
 * Author George S. McIntyre <george@level-n.com>, 2023
 *
 */

#include <string>
#include <algorithm>
#include <atomic>

#include <special.h>
#include <epicsTime.h>
#include <epicsStdlib.h>
#include <epicsString.h>

#include "alarm.h"
#include "iocsource.h"
#include "dbentry.h"
#include "dberrormessage.h"
#include "typeutils.h"
#include "credentials.h"
#include "securityclient.h"
#include "securitylogger.h"
#include "utilpvt.h"

// include last to avoid clash of #define printf with other headers
#include <epicsStdio.h>

namespace pvxs {
namespace ioc {


bool IOCSource::enabled()
{
    /* -1 - disabled
     *  0 - lazy init, check environment
     *  1 - enabled
     */
    static std::atomic<int> ena{};

    auto e = ena.load();
    if(e==0) {
        e = inUnitTest() ? 1 : -1; // default to disabled normally (not unittest)

        auto env_dis = getenv("EPICS_IOC_IGNORE_SERVERS");
        auto env_ena = getenv("PVXS_QSRV_ENABLE");

        if(env_dis && strstr(env_dis, "qsrv2")) {
            e = -1;

        } else if(env_ena && epicsStrCaseCmp(env_ena, "YES")==0) {
            e = 1;

        } else if(env_ena && epicsStrCaseCmp(env_ena, "NO")==0) {
            e = -1;

        } else if(env_ena) {
            // will be seen during initialization, print synchronously
            fprintf(stderr, "ERROR: PVXS_QSRV_ENABLE=%s not YES/NO.  Defaulting to %s.\n",
                    env_ena,
                    e==1 ? "YES" : "NO");
        }
        printf("INFO: PVXS QSRV2 is loaded and %s\n",
               e==1 ? "ENABLED." : "disabled.\n"
               "      To enable set: epicsEnvSet(\"PVXS_QSRV_ENABLE\",\"YES\")\n"
               "      and ensure that $EPICS_IOC_IGNORE_SERVERS does not contain \"qsrv2\".");
        ena = e;
    }
    return e==1;
}

void IOCSource::initialize(Value& value, const MappingInfo &info, dbChannel *chan)
{
    if(info.type==MappingInfo::Scalar) {
        if(auto fld = value["display.form.choices"]) {
            shared_array<const std::string> choices({
                                                        "Default",
                                                        "String",
                                                        "Binary",
                                                        "Decimal",
                                                        "Hex",
                                                        "Exponential",
                                                        "Engineering",
                                                    });
            fld = choices;

            if(dbIsValueField(dbChannelFldDes(chan))) { // only apply Q:form to VAL
                DBEntry ent(dbChannelRecord(chan));

                if(auto tag = ent.info("Q:form")) {
                    for(auto i : range(choices.size())) {
                        if(choices[i] == tag) {
                            value["display.form.index"] = i;
                            break;
                        }
                    }
                }
            }
        }
    }
}

static
void getScalarValue(dbChannel* pChannel,
                    db_field_log *pfl,
                    Value& value)
{
    long nReq = 1;
    union {
        double _align;
        char _size[MAX_STRING_SIZE];
        char str[MAX_STRING_SIZE];
    } buf;

    DBErrorMessage dbErrorMessage(dbChannelGet(pChannel, dbChannelFinalFieldType(pChannel),
                                               &buf, nullptr, &nReq, pfl));
    if (dbErrorMessage) {
        throw std::runtime_error(SB()<<dbChannelName(pChannel)<<" "<<__func__<<" ERROR : "<<dbErrorMessage.c_str());
    } else if(nReq==0) {
        // this was an actual max length 1 array, which has zero elements now.
        memset(&buf, 0, sizeof(buf));
    }

    switch(value.type().code) {
    case TypeCode::String:
        buf.str[sizeof(buf.str)-1] = '\0';
        value = buf.str;
        break;
#define CASE(ENUM, TYPE) \
    case TypeCode::ENUM: value.from(*(const TYPE*)&buf); break
        CASE(Int8, int8_t);
        CASE(Int16, int16_t);
        CASE(Int32, int32_t);
        CASE(Int64, int64_t);
        CASE(UInt8, uint8_t);
        CASE(UInt16, uint16_t);
        CASE(UInt32, uint32_t);
        CASE(UInt64, uint64_t);
        CASE(Float32, float);
        CASE(Float64, double);
#undef CASE
    case TypeCode::Struct:
        if(auto index = value["index"]) {
            if(dbChannelFinalFieldType(pChannel)==DBR_ENUM) {
                index.from(*(epicsEnum16*)&buf);
                break;
            }
        }
    default:
        throw std::logic_error(SB()<<__func__<<" unsupported "<<value.type());
    }
}

static
void getArrayValue(dbChannel* pChannel,
                         db_field_log *pfl,
                         Value& value)
{
    auto final_type(dbChannelFinalFieldType(pChannel));
    auto buf(std::make_shared<std::vector<char>>(dbChannelFinalElements(pChannel) * dbChannelFinalFieldSize(pChannel)));
    long nReq = dbChannelFinalElements(pChannel);

    DBErrorMessage dbErrorMessage(dbChannelGet(pChannel, final_type,
                                               buf->data(), nullptr, &nReq, pfl));
    if (dbErrorMessage) {
        throw std::runtime_error(SB()<<dbChannelName(pChannel)<<" "<<__func__<<" ERROR : "<<dbErrorMessage.c_str());
    }

    if(final_type == DBR_CHAR && value.type()==TypeCode::String && buf->size() >= 1u) {
        // long string
        (*buf)[buf->size()-1u] = '\0'; // paranoia?
        value = std::string(buf->data());

    } else if(final_type == DBR_STRING) {
        shared_array<std::string> arr(nReq);

        for(long n = 0; n < nReq; n++) {
            auto sval = &(*buf)[n*MAX_STRING_SIZE];
            auto nlen = strnlen(sval, MAX_STRING_SIZE);
            arr[n] = std::string(sval, nlen);
        }

        value.from(arr.freeze());
    } else {
        std::shared_ptr<char> cbuf(buf, buf->data());
        buf.reset(); // TODO: c++14 adds aliasing ctor by move()
        shared_array<void> arr(cbuf, nReq, value.type().arrayType());
        cbuf.reset();

        value.from(arr.freeze());
    }
}

// update timeStamp.* and maybe alarm.*
static
void getTimeAlarm(dbChannel* pChannel,
                  db_field_log *pfl,
                  Value& node,
                  const MappingInfo& info,
                  UpdateType::type change)
{
    long nReq = 0;
    long options = DBR_STATUS | DBR_AMSG | DBR_TIME | DBR_UTAG;
    struct ValueTimeAlarm {
        DBRstatus
        DBRamsg
        DBRtime
        DBRutag
    } meta;

    DBErrorMessage dbErrorMessage(dbChannelGet(pChannel, dbChannelFinalFieldType(pChannel),
                                               &meta, &options, &nReq, pfl));
    if (dbErrorMessage) {
        throw std::runtime_error(SB()<<dbChannelName(pChannel)<<" "<<__func__<<" ERROR : "<<dbErrorMessage.c_str());
    }
    // options may be updated.
    // as of base 7.0.6 time/alarm meta-data is always available

    if(change & UpdateType::Alarm) {
        const char* stsmsg = nullptr;
        if(options & DBR_STATUS) {
            // PVA status != DB status
            uint32_t status;
            switch(meta.status) {
            case NO_ALARM:
                status = 0;
                break;
            case READ_ALARM:
            case WRITE_ALARM:
            case HIHI_ALARM:
            case HIGH_ALARM:
            case LOLO_ALARM:
            case LOW_ALARM:
            case STATE_ALARM:
            case COS_ALARM:
            case HW_LIMIT_ALARM:
                status = 1; // DEVICE
                break;
            case COMM_ALARM:
            case TIMEOUT_ALARM:
            case UDF_ALARM:
                status = 2; // DRIVER
                break;
            case CALC_ALARM:
            case SCAN_ALARM:
            case LINK_ALARM:
            case SOFT_ALARM:
            case BAD_SUB_ALARM:
                status = 3; // RECORD
                break;
            case DISABLE_ALARM:
            case SIMM_ALARM:
            case READ_ACCESS_ALARM:
            case WRITE_ACCESS_ALARM:
                status = 4; // DB
                break;
            default:
                status = 6; // UNDEFINED
            }

            if(meta.status < ALARM_NSTATUS)
                stsmsg = epicsAlarmConditionStrings[meta.status];
            node["alarm.status"] = status;
            node["alarm.severity"] = meta.severity;
        }
#if DBR_AMSG
        if((options & DBR_AMSG) && meta.amsg[0]) {
            node["alarm.message"] = meta.amsg;
        } else
#endif
        {
            node["alarm.message"] = meta.status && stsmsg ? stsmsg : "";
        }
    } // DBE_ALARM
    if(options & DBR_TIME) {
        node["timeStamp.secondsPastEpoch"] = meta.time.secPastEpoch + POSIX_TIME_AT_EPICS_EPOCH;
        node["timeStamp.nanoseconds"] = meta.time.nsec & ~info.nsecMask;
    }
#if DBR_UTAG
    if(options & DBR_UTAG) {
        auto utag = meta.utag;
        if(info.nsecMask)
            utag = meta.time.nsec & info.nsecMask;
        node["timeStamp.userTag"] = utag;
    }
#endif
}

static
void getProperties(dbChannel* pChannel, db_field_log *pfl, Value& node)
{
    struct PropertyMeta {
        DBRunits
        DBRprecision
        DBRenumStrs
        DBRgrDouble
        DBRctrlDouble
        DBRalDouble
    } meta;
    long options = DBR_UNITS | DBR_PRECISION | DBR_ENUM_STRS | DBR_GR_DOUBLE | DBR_CTRL_DOUBLE | DBR_AL_DOUBLE;
    auto dbr_type = dbChannelFinalFieldType(pChannel);
    long nReq = 0; // only meta.  (so DBF type ignored)

    DBErrorMessage dbErrorMessage(dbChannelGet(pChannel, dbr_type,
                                               &meta, &options, &nReq, pfl));
    if (dbErrorMessage) {
        throw std::runtime_error(SB()<<dbChannelName(pChannel)<<" "<<__func__<<" ERROR : "<<dbErrorMessage.c_str());
    }
    // options has been updated to reflect meta-data actually updated.
    if(options & DBR_UNITS) {
        if(auto units = node["display.units"])
            units = meta.units;
    }
    if(options & DBR_PRECISION) {
        // TODO: add in nt.cpp?
    }
    if(options & DBR_ENUM_STRS) {
        if(auto choices = node["value.choices"]) {
            shared_array<std::string> arr(meta.no_str);
            for (epicsUInt32 i = 0; i < meta.no_str; i++) {
                arr[i] = meta.strs[i];
            }
            choices.from(arr.freeze());
        }
    }
    if(auto dlL = node["display.limitLow"]) { // if numeric
        if(options & DBR_GR_DOUBLE) {
            dlL = meta.lower_disp_limit;
            node["display.limitHigh"] = meta.upper_disp_limit;
        }
        if(options & DBR_CTRL_DOUBLE) {
            node["control.limitLow"] = meta.lower_ctrl_limit;
            node["control.limitHigh"] = meta.upper_ctrl_limit;
        }
        if(options & DBR_AL_DOUBLE) {
            node["valueAlarm.lowAlarmLimit"] = meta.lower_alarm_limit;
            node["valueAlarm.lowWarningLimit"] = meta.lower_warning_limit;
            node["valueAlarm.highWarningLimit"] = meta.upper_warning_limit;
            node["valueAlarm.highAlarmLimit"] = meta.upper_alarm_limit;
        }
    }
    if(true) { // cheating at the moment.  DESC is not marked DBE_PROPERTY
        if(auto desc = node["display.description"])
            desc = dbChannelRecord(pChannel)->desc;
    }
}

void IOCSource::get(Value& node, // node within top level structure addressed by Field::fieldName
                    const MappingInfo &info,
                    const Value& anyType,
                    UpdateType::type change,
                    dbChannel *pChannel, // which type of event
                    db_field_log* pDbFieldLog)
{
    if(info.type==MappingInfo::Proc || info.type==MappingInfo::Structure)
        return;

    if(info.type==MappingInfo::Const) {
        node.assign(info.cval);
        return;
    }

    if((change & UpdateType::Property) && info.type==MappingInfo::Scalar) {
        getProperties(pChannel, pDbFieldLog, node);
    }

    if((info.type==MappingInfo::Scalar || info.type==MappingInfo::Meta) && (change & (UpdateType::Value | UpdateType::Alarm))) {
        getTimeAlarm(pChannel, pDbFieldLog, node, info, change);
    }

    if((change & UpdateType::Value) && info.type!=MappingInfo::Meta) {
        Value value;
        if(info.type==MappingInfo::Scalar) {
            value = node["value"];
        } else if(info.type==MappingInfo::Any) {
            value = anyType.cloneEmpty();
            node.from(value);
        } else {
            value = node;
        }

        if(dbChannelFinalElements(pChannel)==1) {
            getScalarValue(pChannel, pDbFieldLog, value);
        } else {
            getArrayValue(pChannel, pDbFieldLog, value);
        }
    }
}

/**
 * Do necessary preprocessing before put operations.  Check if put is allowed.
 *
 * @param pDbChannel channel to do preprocessing for
 * @param securityLogger the logger that will audit security events
 * @param credentials client credentials that are applied to this execution context
 * @param securityClient the security client.  Keep in scope around the put operation
 */
void
IOCSource::doPreProcessing(dbChannel* pDbChannel, SecurityLogger& securityLogger, const Credentials& credentials,
        const SecurityClient& securityClient) {
    if (pDbChannel->addr.special == SPC_ATTRIBUTE) {
        throw std::runtime_error("Unable to put value: Modifications not allowed: S_db_noMod");
    } else if (pDbChannel->addr.precord->disp && pDbChannel->addr.pfield != &pDbChannel->addr.precord->disp) {
        throw std::runtime_error("Unable to put value: Field Disabled: S_db_putDisabled");
    }

    SecurityLogger asWritePvt(
            asTrapWriteWithData((securityClient.cli)[0], // The user is the first element
                    credentials.cred[0].c_str(),         // The user is the first element
                    credentials.host.c_str(),
                    pDbChannel,
                    dbChannelFinalFieldType(pDbChannel),
                    dbChannelFinalElements(pDbChannel),
                    nullptr
            )
    );

    securityLogger.swap(asWritePvt);

}

/**
 * Do necessary preprocessing before put operations.  Check if put is allowed.
 *
 * @param securityClient security client applied to this execution context
 */
void IOCSource::doFieldPreProcessing(const SecurityClient& securityClient) {
    if (!securityClient.canWrite()) {
        // TODO this will abort the whole group put operation, so may be a behavior change, need to check
        throw std::runtime_error("Put not permitted");
    }
}

/**
 * Do necessary post processing after put operations.  If this field is a processing record then do processing
 * and set status
 * Note: Only called when dbPutField() is not called.
 *
 * @param pDbChannel channel to do post processing for
 * @param forceProcessing whether to force processing, True, False
 */
void IOCSource::doPostProcessing(dbChannel* pDbChannel, TriState forceProcessing) {
    if (pDbChannel->addr.pfield == &pDbChannel->addr.precord->proc ||
            (forceProcessing == True) ||
            (pDbChannel->addr.pfldDes->process_passive &&
                    pDbChannel->addr.precord->scan == 0 &&
                    dbChannelFinalFieldSize(pDbChannel) < DBR_PUT_ACKT &&
                    forceProcessing == Unset)) {
        if (pDbChannel->addr.precord->pact) {
#if EPICS_VERSION_INT >= VERSION_INT(3, 16, 2, 0)
            if (dbAccessDebugPUTF && pDbChannel->addr.precord->tpro) {
                printf("%s: single source onPut to Active '%s', setting RPRO=1\n",
                        epicsThreadGetNameSelf(), pDbChannel->addr.precord->name);
            }
#endif
            pDbChannel->addr.precord->rpro = TRUE;
        } else {
            pDbChannel->addr.precord->putf = TRUE;
            DBErrorMessage dbErrorMessage(dbProcess(pDbChannel->addr.precord));
            if (dbErrorMessage) {
                throw std::runtime_error(dbErrorMessage.c_str());
            }
        }
    }
}

/**
 * Set a flag that will force processing of record in the specified security control object
 *
 * @param pvRequest the request
 * @param securityControlObject the security control object to update
 */
void IOCSource::setForceProcessingFlag(const Value& pvRequest,
        const std::shared_ptr<SecurityControlObject>& securityControlObject) {
    pvRequest["record._options.process"]
            .as<std::string>([&securityControlObject](const std::string& forceProcessingOption) {
                if (forceProcessingOption == "true") {
                    securityControlObject->forceProcessing = True;
                } else if (forceProcessingOption == "false") {
                    securityControlObject->forceProcessing = False;
                }
            });
}

static
void doDbPut(dbChannel* pDbChannel, short dbr, const void *pValueBuffer, size_t nElements)
{
    long status;
    if (dbChannelFieldType(pDbChannel) >= DBF_INLINK && dbChannelFieldType(pDbChannel) <= DBF_FWDLINK) {
        status = dbChannelPutField(pDbChannel, dbr, pValueBuffer, nElements);
    } else {
        status = dbChannelPut(pDbChannel, dbr, pValueBuffer, nElements);
    }
    DBErrorMessage dbErrorMessage(status);
    if (dbErrorMessage) {
        throw std::runtime_error(dbErrorMessage.c_str());
    }
}

static
void putScalar(dbChannel* pDbChannel, const Value& value)
{
    union {
        epicsUInt8 UCHAR;
        epicsUInt16 USHORT;
        epicsUInt32 ULONG;
        epicsUInt64 UINT64;
        epicsInt8 CHAR;
        epicsInt16 SHORT;
        epicsInt32 LONG;
        epicsInt64 INT64;
        float FLOAT;
        double DOUBLE;
        char STRING[MAX_STRING_SIZE];
    } buf;

    switch(dbChannelFinalFieldType(pDbChannel)) {
#define CASE(DBR, CTYPE) case DBR_ ## DBR: buf.DBR = value.as<CTYPE>(); break
    CASE(CHAR, int8_t);
    case DBR_ENUM:
    CASE(SHORT, int16_t);
    CASE(LONG, int32_t);
    CASE(UCHAR, uint8_t);
    CASE(USHORT, uint16_t);
    CASE(ULONG, uint32_t);
#ifdef DBR_INT64
    CASE(INT64, int64_t);
    CASE(UINT64, uint64_t);
#endif
    CASE(FLOAT, float);
    CASE(DOUBLE, double);
#undef CASE
    case DBR_STRING:
    {
        auto s(value.as<std::string>());
        strncpy(buf.STRING, s.c_str(), MAX_STRING_SIZE-1u);
        buf.STRING[MAX_STRING_SIZE-1u] = '\0';
        break;
    }
    default:
        throw std::logic_error(SB()<<__func__<<" unhandled case "<<dbChannelFinalFieldType(pDbChannel));
    }

    doDbPut(pDbChannel, dbChannelFinalFieldType(pDbChannel), &buf, 1u);
}

static
void putLongString(dbChannel* pDbChannel, const Value& value)
{
    auto str(value.as<std::string>());
    auto N = str.size()+1u; // include NIL

    doDbPut(pDbChannel, DBR_CHAR, str.c_str(), N);
}

static
void putStringArray(dbChannel* pDbChannel, const Value& value)
{
    auto arr = value.as<shared_array<const std::string>>();

    std::vector<char> buf(MAX_STRING_SIZE * arr.size());

    char* pCurrent = buf.data();
    for (auto& element: arr) {
        element.copy(pCurrent, MAX_STRING_SIZE - 1);
        pCurrent += MAX_STRING_SIZE;
    }

    doDbPut(pDbChannel, DBR_STRING, buf.data(), arr.size());
}

static
void putArray(dbChannel* pDbChannel, const Value& value)
{
    auto arr = value.as<shared_array<const void>>();

    short dbr;
    switch(arr.original_type()) {
    case ArrayType::Null:
        return;
    case ArrayType::Bool:
    case ArrayType::Value:
    default:
        throw std::runtime_error(SB()<<"Unsupported "<<__func__<<" from "<<arr.original_type());
    case ArrayType::Int8:    dbr = DBR_CHAR; break;
    case ArrayType::Int16:   dbr = DBR_SHORT; break;
    case ArrayType::Int32:   dbr = DBR_LONG; break;
    case ArrayType::UInt8:   dbr = DBR_UCHAR; break;
    case ArrayType::UInt16:  dbr = DBR_USHORT; break;
    case ArrayType::UInt32:  dbr = DBR_ULONG; break;
#ifdef DBR_INT64
    case ArrayType::Int64:   dbr = DBR_INT64; break;
    case ArrayType::UInt64:  dbr = DBR_UINT64; break;
#endif
    case ArrayType::Float32: dbr = DBR_FLOAT; break;
    case ArrayType::Float64: dbr = DBR_DOUBLE; break;
    case ArrayType::String:
        putStringArray(pDbChannel, value);
        return;
    }

    doDbPut(pDbChannel, dbr, arr.data(), arr.size());
}

/**
 * Put a given value to the specified channel.  Throw an exception if there are any errors.
 *
 * @param pDbChannel the channel to put the value into
 * @param value the value to put
 */
void IOCSource::put(dbChannel* pDbChannel, const Value& node, MappingInfo info) {
    Value value;
    switch(info.type) {
    case MappingInfo::Meta:
    case MappingInfo::Proc:
    case MappingInfo::Structure:
        return; // can't write
    case MappingInfo::Any:
        value = node["->"]; // de-ref into Any
        break;
    case MappingInfo::Plain:
        value = node;
        break;
    case MappingInfo::Scalar:
        value = node["value"];
        if(value.type()==TypeCode::Struct)
            value = value["index"]; // NTEnum
        break;
    case MappingInfo::Const:
        value = info.cval;
        break;
    }

    if (dbChannelFinalElements(pDbChannel) == 1) {
        putScalar(pDbChannel, value);
    } else if(dbChannelFinalFieldType(pDbChannel) == DBR_CHAR && value.type()==TypeCode::String) {
        putLongString(pDbChannel, value);
    } else if(dbChannelFinalFieldType(pDbChannel) == DBR_STRING) {
        putStringArray(pDbChannel, value);
    } else {
        putArray(pDbChannel, value);
    }
}

/**
 * Utility function to get the TypeCode that the given database channel is configured for
 *
 * @param pDbChannel the pointer to the database channel to get the TypeCode for
 * @param errOnLinks determines whether to throw an error on finding links, default no
 * @return the TypeCode that the channel is configured for
 */
TypeCode IOCSource::getChannelValueType(const dbChannel* chan, const bool errOnLinks) {
    /* for links, could check dbChannelFieldType().
     * for long strings, dbChannelCreate() '$' handling overwrites dbAddr::field_type
     *   (for some reason...)
     */
    if(!chan)
        throw std::runtime_error("Missing required +channel");
    auto field_type(dbChannelFldDes(chan)->field_type);
    auto final_field_type(dbChannelFinalFieldType(chan));

    if(errOnLinks && field_type >= DBF_INLINK && field_type <= DBF_OUTLINK)
        throw std::runtime_error("Link fields not allowed in this context");

    bool isArray = dbChannelFinalElements(chan)!=1;
    bool stringlike = (field_type >= DBF_INLINK && field_type <= DBF_OUTLINK) || field_type == DBF_STRING;

    // string-like field being treated as single char[].  aka. long string
    if(stringlike && final_field_type==DBR_CHAR && isArray)
        return TypeCode::String;

    TypeCode valueType(fromDbrType(final_field_type));

    if(isArray)
        valueType = valueType.arrayOf();

    return valueType;
}

static
thread_local server::ExecOp* currentOp;

CurrentOp::CurrentOp(server::ExecOp *op)
    :prev(currentOp)
{
    currentOp = op;
}

CurrentOp::~CurrentOp()
{
    currentOp = prev;
}

server::ExecOp*
CurrentOp::current()
{
    return currentOp;
}

} // pvxs
} // ioc
