/*
 * h2_mod_mex.cpp
 *
 * Generic MATLAB MEX wrapper for H2 modulation APIs.
 *
 * This file wraps the modulation call sequence inside C++:
 *   device_open_usb -> mod_open -> mod_init_xxx -> mod_config_xxx ->
 *   mod_generate_stream -> mod_close -> device_close
 *
 * MATLAB usage:
 *   [iqData, sampleRate, info, profileOut] = h2_mod_mex('generate', modType, profile, symbolLength, devNum, apiDir)
 *
 * modType:
 *   'digitalmod', 'am', 'fm', 'dsss', 'pulse', 'multitone',
 *   'stepsweep', 'rampsweep', 'ofdm', 'awgn'
 *
 * Build in MATLAB on Windows:
 *   mex -setup C++
 *   mex -v -largeArrayDims h2_api\h2_mod_mex.cpp -outdir h2_api
 *
 * Put the compiled h2_mod_mex.mexw64 in the same folder as:
 *   h2_api.dll, genSignalWave.dll
 */

#include "mex.h"
#include "matrix.h"

#include <windows.h>
#include <stdint.h>
#include <string>
#include <algorithm>
#include <sstream>
#include <vector>
#include <cstring>
#include <cctype>

/* ===========================
 * Type definitions copied from h2_api_matlab_single.h
 * =========================== */

typedef uint8_t state;
typedef uint8_t channel_type;

typedef struct
{
    uint16_t model;
    uint64_t uid_l64;
    uint32_t uid_h32;
    uint16_t hardware_version;
    uint32_t mfw_version;
    uint32_t ffw_version;
    uint32_t pmu_version;
    uint32_t agu_version;
    uint32_t bus_version;
    uint32_t eio_version;
    uint32_t bus_bandwidth;
} device_info;

typedef struct
{
    void **device;
    uint16_t num;
    channel_type type;
} channel;

typedef enum
{
    DigitalModType_BPSK     = 1,
    DigitalModType_DBPSK    = 2,
    DigitalModType_QPSK     = 3,
    DigitalModType_DQPSK    = 4,
    DigitalModType_OQPSK    = 5,
    DigitalModType_Pi4DQPSK = 6,
    DigitalModType_PSK8     = 7,
    DigitalModType_D8PSK    = 8,
    DigitalModType_PSK16    = 9,
    DigitalModType_QAM16    = 10,
    DigitalModType_QAM64    = 11,
    DigitalModType_QAM256   = 12,
    DigitalModType_QAM1024  = 13,
    DigitalModType_ASK2     = 14,
    DigitalModType_FSK2     = 15,
    DigitalModType_FSK4     = 16,
    DigitalModType_FSK8     = 17,
    DigitalModType_FSK16    = 18,
    DigitalModType_ASK4     = 19,
    DigitalModType_ASK8     = 20,
    DigitalModType_APSK16   = 21,
    DigitalModType_QAM8     = 22
} mod_digitalmod_type;

typedef enum
{
    FilterType_Rectangular      = 1,
    FilterType_RaisedCosine     = 2,
    FilterType_RootRaisedCosine = 3,
    FilterType_Gaussian         = 4,
    FilterType_HalfSine         = 5
} mod_filter_type;

typedef enum
{
    ModShape_Sine       = 1,
    ModShape_Square     = 2,
    ModShape_Triangle   = 3,
    ModShape_Ramp       = 4
} mod_shape_type;

typedef struct
{
    mod_digitalmod_type digitalmod_type;
    double symbolrate;
    double fskdeviation;
    int32_t pn;
    int32_t sequenceseed;
    mod_filter_type filter_type;
    int32_t filter_symbols;
    int32_t sps;
    double alpha;
} digitalmod_profile;

typedef struct
{
    double rate;
    double depth;
    mod_shape_type shape;
} am_profile;

typedef struct
{
    double rate;
    double deviation;
    mod_shape_type shape;
} fm_profile;

typedef struct
{
    double symbolrate;
    mod_digitalmod_type digitalmod_type;
    int32_t code;
    int32_t filter_symbols;
    int32_t sps;
    double alpha;
    int32_t sequenceseed;
    mod_filter_type filter_type;
} dsss_profile;

typedef struct
{
    double idleinterval;
    int32_t fftsize;
    int32_t symbolcount;
    int32_t guardbandcarriers_left;
    int32_t guardbandcarriers_right;
    int32_t guardinterval;
    int32_t window_length;
    uint8_t windowed;
    mod_digitalmod_type digitalmod_type;
    uint8_t nulldc;
    double samplerate;
} ofdm_profile;

typedef struct
{
    double width;
    double period;
} pulse_profile;

typedef struct
{
    double tonephase;
    int32_t tonecount;
    double freqspacing;
} multitone_profile;

typedef struct
{
    double center;
    double span;
    int32_t points;
    double dwelltime;
} stepsweep_profile;

typedef struct
{
    double span;
    double sweeptime;
    double period;
} rampsweep_profile;

typedef struct
{
    double bandwidth;
    double length;
} awgn_profile;

/* ===========================
 * DLL function pointer types
 * =========================== */

typedef int  (*device_open_usb_t)(void **device, uint8_t dnum, void *ch_o, uint16_t *chn, device_info *dinfo);
typedef int  (*device_close_t)(void **device);

typedef int  (*mod_open_t)(void **device, int8_t *version_o);
typedef int  (*mod_close_t)(void **device);

typedef void (*mod_init_digitalmod_t)(void **device, digitalmod_profile *profile_o);
typedef void (*mod_init_am_t)(void **device, am_profile *profile_o);
typedef void (*mod_init_fm_t)(void **device, fm_profile *profile_o);
typedef void (*mod_init_dsss_t)(void **device, dsss_profile *profile_o);
typedef void (*mod_init_pulse_t)(void **device, pulse_profile *profile_o);
typedef void (*mod_init_multitone_t)(void **device, multitone_profile *profile_o);
typedef void (*mod_init_stepsweep_t)(void **device, stepsweep_profile *profile_o);
typedef void (*mod_init_rampsweep_t)(void **device, rampsweep_profile *profile_o);
typedef void (*mod_init_ofdm_t)(void **device, ofdm_profile *profile_o);
typedef void (*mod_init_awgn_t)(void **device, awgn_profile *profile_o);

typedef int  (*mod_config_digitalmod_t)(void **device, const digitalmod_profile *profile_in, digitalmod_profile *profile_o);
typedef int  (*mod_config_am_t)(void **device, const am_profile *profile_in, am_profile *profile_o);
typedef int  (*mod_config_fm_t)(void **device, const fm_profile *profile_in, fm_profile *profile_o);
typedef int  (*mod_config_dsss_t)(void **device, const dsss_profile *profile_in, dsss_profile *profile_o);
typedef int  (*mod_config_pulse_t)(void **device, const pulse_profile *profile_in, pulse_profile *profile_o);
typedef int  (*mod_config_multitone_t)(void **device, const multitone_profile *profile_in, multitone_profile *profile_o);
typedef int  (*mod_config_stepsweep_t)(void **device, const stepsweep_profile *profile_in, stepsweep_profile *profile_o);
typedef int  (*mod_config_rampsweep_t)(void **device, const rampsweep_profile *profile_in, rampsweep_profile *profile_o);
typedef int  (*mod_config_ofdm_t)(void **device, const ofdm_profile *profile_in, ofdm_profile *profile_o);
typedef int  (*mod_config_awgn_t)(void **device, const awgn_profile *profile_in, awgn_profile *profile_o);

typedef int  (*mod_generate_stream_t)(void **device, int32_t symbol_length, int16_t **iq_o, int32_t *length_o, double *sample_rate_o);

struct Api
{
    HMODULE h2 = NULL;
    HMODULE gen = NULL;

    device_open_usb_t device_open_usb = NULL;
    device_close_t device_close = NULL;

    mod_open_t mod_open = NULL;
    mod_close_t mod_close = NULL;

    mod_init_digitalmod_t mod_init_digitalmod = NULL;
    mod_init_am_t mod_init_am = NULL;
    mod_init_fm_t mod_init_fm = NULL;
    mod_init_dsss_t mod_init_dsss = NULL;
    mod_init_pulse_t mod_init_pulse = NULL;
    mod_init_multitone_t mod_init_multitone = NULL;
    mod_init_stepsweep_t mod_init_stepsweep = NULL;
    mod_init_rampsweep_t mod_init_rampsweep = NULL;
    mod_init_ofdm_t mod_init_ofdm = NULL;
    mod_init_awgn_t mod_init_awgn = NULL;

    mod_config_digitalmod_t mod_config_digitalmod = NULL;
    mod_config_am_t mod_config_am = NULL;
    mod_config_fm_t mod_config_fm = NULL;
    mod_config_dsss_t mod_config_dsss = NULL;
    mod_config_pulse_t mod_config_pulse = NULL;
    mod_config_multitone_t mod_config_multitone = NULL;
    mod_config_stepsweep_t mod_config_stepsweep = NULL;
    mod_config_rampsweep_t mod_config_rampsweep = NULL;
    mod_config_ofdm_t mod_config_ofdm = NULL;
    mod_config_awgn_t mod_config_awgn = NULL;

    mod_generate_stream_t mod_generate_stream = NULL;
};

static std::string lowerString(std::string s)
{
    std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
    return s;
}

static bool fileExists(const std::string &path)
{
    DWORD attr = GetFileAttributesA(path.c_str());
    return (attr != INVALID_FILE_ATTRIBUTES) && !(attr & FILE_ATTRIBUTE_DIRECTORY);
}

static std::string joinPath(const std::string &dir, const std::string &name)
{
    if (dir.empty()) return name;
    char last = dir[dir.size() - 1];
    if (last == '\\' || last == '/') return dir + name;
    return dir + "\\" + name;
}

static std::string mxToString(const mxArray *a, const char *argName)
{
    if (!mxIsChar(a)) {
        mexErrMsgIdAndTxt("h2_mod_mex:InvalidInput", "%s must be a string.", argName);
    }
    char *p = mxArrayToString(a);
    if (!p) {
        mexErrMsgIdAndTxt("h2_mod_mex:InvalidInput", "Failed to convert %s to string.", argName);
    }
    std::string s(p);
    mxFree(p);
    return s;
}

static double getScalarField(const mxArray *s, const char *name, double def)
{
    if (!s || !mxIsStruct(s)) return def;
    mxArray *v = mxGetField(s, 0, name);
    if (!v || mxIsEmpty(v)) return def;
    if (!mxIsNumeric(v) || mxIsComplex(v) || mxGetNumberOfElements(v) < 1) {
        mexErrMsgIdAndTxt("h2_mod_mex:InvalidProfile", "Profile field '%s' must be a real numeric scalar.", name);
    }
    return mxGetScalar(v);
}

static int32_t getInt32Field(const mxArray *s, const char *name, int32_t def)
{
    return static_cast<int32_t>(getScalarField(s, name, static_cast<double>(def)));
}

static uint8_t getUint8Field(const mxArray *s, const char *name, uint8_t def)
{
    double v = getScalarField(s, name, static_cast<double>(def));
    if (v < 0 || v > 255) {
        mexErrMsgIdAndTxt("h2_mod_mex:InvalidProfile", "Profile field '%s' must be in [0,255].", name);
    }
    return static_cast<uint8_t>(v);
}

static mxArray *makeStruct(const std::vector<const char*> &fields)
{
    /*
     * MATLAB R2016a declares mxCreateStructMatrix as taking const char **.
     * If fields is a const vector, fields.data() has type const char * const *,
     * which MinGW rejects. Make a local non-const copy of the pointer array so
     * &local[0] has type const char **. The strings themselves remain const.
     */
    std::vector<const char*> local(fields.begin(), fields.end());
    const char **names = local.empty() ? NULL : &local[0];
    return mxCreateStructMatrix(1, 1, static_cast<int>(local.size()), names);
}

static void setFieldDouble(mxArray *s, const char *name, double v)
{
    mxSetField(s, 0, name, mxCreateDoubleScalar(v));
}

static void setFieldInt(mxArray *s, const char *name, int32_t v)
{
    mxSetField(s, 0, name, mxCreateDoubleScalar(static_cast<double>(v)));
}

static void setFieldString(mxArray *s, const char *name, const std::string &v)
{
    mxSetField(s, 0, name, mxCreateString(v.c_str()));
}

static mxArray *profileToMx(const digitalmod_profile &p)
{
    std::vector<const char*> f = {"digitalmod_type","symbolrate","fskdeviation","pn","sequenceseed","filter_type","filter_symbols","sps","alpha"};
    mxArray *s = makeStruct(f);
    setFieldInt(s,"digitalmod_type",static_cast<int32_t>(p.digitalmod_type));
    setFieldDouble(s,"symbolrate",p.symbolrate);
    setFieldDouble(s,"fskdeviation",p.fskdeviation);
    setFieldInt(s,"pn",p.pn);
    setFieldInt(s,"sequenceseed",p.sequenceseed);
    setFieldInt(s,"filter_type",static_cast<int32_t>(p.filter_type));
    setFieldInt(s,"filter_symbols",p.filter_symbols);
    setFieldInt(s,"sps",p.sps);
    setFieldDouble(s,"alpha",p.alpha);
    return s;
}

static mxArray *profileToMx(const am_profile &p)
{
    std::vector<const char*> f = {"rate","depth","shape"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"rate",p.rate);
    setFieldDouble(s,"depth",p.depth);
    setFieldInt(s,"shape",static_cast<int32_t>(p.shape));
    return s;
}

static mxArray *profileToMx(const fm_profile &p)
{
    std::vector<const char*> f = {"rate","deviation","shape"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"rate",p.rate);
    setFieldDouble(s,"deviation",p.deviation);
    setFieldInt(s,"shape",static_cast<int32_t>(p.shape));
    return s;
}

static mxArray *profileToMx(const dsss_profile &p)
{
    std::vector<const char*> f = {"symbolrate","digitalmod_type","code","filter_symbols","sps","alpha","sequenceseed","filter_type"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"symbolrate",p.symbolrate);
    setFieldInt(s,"digitalmod_type",static_cast<int32_t>(p.digitalmod_type));
    setFieldInt(s,"code",p.code);
    setFieldInt(s,"filter_symbols",p.filter_symbols);
    setFieldInt(s,"sps",p.sps);
    setFieldDouble(s,"alpha",p.alpha);
    setFieldInt(s,"sequenceseed",p.sequenceseed);
    setFieldInt(s,"filter_type",static_cast<int32_t>(p.filter_type));
    return s;
}

static mxArray *profileToMx(const ofdm_profile &p)
{
    std::vector<const char*> f = {"idleinterval","fftsize","symbolcount","guardbandcarriers_left","guardbandcarriers_right","guardinterval","window_length","windowed","digitalmod_type","nulldc","samplerate"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"idleinterval",p.idleinterval);
    setFieldInt(s,"fftsize",p.fftsize);
    setFieldInt(s,"symbolcount",p.symbolcount);
    setFieldInt(s,"guardbandcarriers_left",p.guardbandcarriers_left);
    setFieldInt(s,"guardbandcarriers_right",p.guardbandcarriers_right);
    setFieldInt(s,"guardinterval",p.guardinterval);
    setFieldInt(s,"window_length",p.window_length);
    setFieldInt(s,"windowed",static_cast<int32_t>(p.windowed));
    setFieldInt(s,"digitalmod_type",static_cast<int32_t>(p.digitalmod_type));
    setFieldInt(s,"nulldc",static_cast<int32_t>(p.nulldc));
    setFieldDouble(s,"samplerate",p.samplerate);
    return s;
}

static mxArray *profileToMx(const pulse_profile &p)
{
    std::vector<const char*> f = {"width","period"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"width",p.width);
    setFieldDouble(s,"period",p.period);
    return s;
}

static mxArray *profileToMx(const multitone_profile &p)
{
    std::vector<const char*> f = {"tonephase","tonecount","freqspacing"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"tonephase",p.tonephase);
    setFieldInt(s,"tonecount",p.tonecount);
    setFieldDouble(s,"freqspacing",p.freqspacing);
    return s;
}

static mxArray *profileToMx(const stepsweep_profile &p)
{
    std::vector<const char*> f = {"center","span","points","dwelltime"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"center",p.center);
    setFieldDouble(s,"span",p.span);
    setFieldInt(s,"points",p.points);
    setFieldDouble(s,"dwelltime",p.dwelltime);
    return s;
}

static mxArray *profileToMx(const rampsweep_profile &p)
{
    std::vector<const char*> f = {"span","sweeptime","period"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"span",p.span);
    setFieldDouble(s,"sweeptime",p.sweeptime);
    setFieldDouble(s,"period",p.period);
    return s;
}

static mxArray *profileToMx(const awgn_profile &p)
{
    std::vector<const char*> f = {"bandwidth","length"};
    mxArray *s = makeStruct(f);
    setFieldDouble(s,"bandwidth",p.bandwidth);
    setFieldDouble(s,"length",p.length);
    return s;
}

static void applyMxToProfile(const mxArray *s, digitalmod_profile &p)
{
    p.digitalmod_type = static_cast<mod_digitalmod_type>(getInt32Field(s,"digitalmod_type",static_cast<int32_t>(p.digitalmod_type)));
    p.symbolrate = getScalarField(s,"symbolrate",p.symbolrate);
    p.fskdeviation = getScalarField(s,"fskdeviation",p.fskdeviation);
    p.pn = getInt32Field(s,"pn",p.pn);
    p.sequenceseed = getInt32Field(s,"sequenceseed",p.sequenceseed);
    p.filter_type = static_cast<mod_filter_type>(getInt32Field(s,"filter_type",static_cast<int32_t>(p.filter_type)));
    p.filter_symbols = getInt32Field(s,"filter_symbols",p.filter_symbols);
    p.sps = getInt32Field(s,"sps",p.sps);
    p.alpha = getScalarField(s,"alpha",p.alpha);
}

static void applyMxToProfile(const mxArray *s, am_profile &p)
{
    p.rate = getScalarField(s,"rate",p.rate);
    p.depth = getScalarField(s,"depth",p.depth);
    p.shape = static_cast<mod_shape_type>(getInt32Field(s,"shape",static_cast<int32_t>(p.shape)));
}

static void applyMxToProfile(const mxArray *s, fm_profile &p)
{
    p.rate = getScalarField(s,"rate",p.rate);
    p.deviation = getScalarField(s,"deviation",p.deviation);
    p.shape = static_cast<mod_shape_type>(getInt32Field(s,"shape",static_cast<int32_t>(p.shape)));
}

static void applyMxToProfile(const mxArray *s, dsss_profile &p)
{
    p.symbolrate = getScalarField(s,"symbolrate",p.symbolrate);
    p.digitalmod_type = static_cast<mod_digitalmod_type>(getInt32Field(s,"digitalmod_type",static_cast<int32_t>(p.digitalmod_type)));
    p.code = getInt32Field(s,"code",p.code);
    p.filter_symbols = getInt32Field(s,"filter_symbols",p.filter_symbols);
    p.sps = getInt32Field(s,"sps",p.sps);
    p.alpha = getScalarField(s,"alpha",p.alpha);
    p.sequenceseed = getInt32Field(s,"sequenceseed",p.sequenceseed);
    p.filter_type = static_cast<mod_filter_type>(getInt32Field(s,"filter_type",static_cast<int32_t>(p.filter_type)));
}

static void applyMxToProfile(const mxArray *s, ofdm_profile &p)
{
    p.idleinterval = getScalarField(s,"idleinterval",p.idleinterval);
    p.fftsize = getInt32Field(s,"fftsize",p.fftsize);
    p.symbolcount = getInt32Field(s,"symbolcount",p.symbolcount);
    p.guardbandcarriers_left = getInt32Field(s,"guardbandcarriers_left",p.guardbandcarriers_left);
    p.guardbandcarriers_right = getInt32Field(s,"guardbandcarriers_right",p.guardbandcarriers_right);
    p.guardinterval = getInt32Field(s,"guardinterval",p.guardinterval);
    p.window_length = getInt32Field(s,"window_length",p.window_length);
    p.windowed = getUint8Field(s,"windowed",p.windowed);
    p.digitalmod_type = static_cast<mod_digitalmod_type>(getInt32Field(s,"digitalmod_type",static_cast<int32_t>(p.digitalmod_type)));
    p.nulldc = getUint8Field(s,"nulldc",p.nulldc);
    p.samplerate = getScalarField(s,"samplerate",p.samplerate);
}

static void applyMxToProfile(const mxArray *s, pulse_profile &p)
{
    p.width = getScalarField(s,"width",p.width);
    p.period = getScalarField(s,"period",p.period);
}

static void applyMxToProfile(const mxArray *s, multitone_profile &p)
{
    p.tonephase = getScalarField(s,"tonephase",p.tonephase);
    p.tonecount = getInt32Field(s,"tonecount",p.tonecount);
    p.freqspacing = getScalarField(s,"freqspacing",p.freqspacing);
}

static void applyMxToProfile(const mxArray *s, stepsweep_profile &p)
{
    p.center = getScalarField(s,"center",p.center);
    p.span = getScalarField(s,"span",p.span);
    p.points = getInt32Field(s,"points",p.points);
    p.dwelltime = getScalarField(s,"dwelltime",p.dwelltime);
}

static void applyMxToProfile(const mxArray *s, rampsweep_profile &p)
{
    p.span = getScalarField(s,"span",p.span);
    p.sweeptime = getScalarField(s,"sweeptime",p.sweeptime);
    p.period = getScalarField(s,"period",p.period);
}

static void applyMxToProfile(const mxArray *s, awgn_profile &p)
{
    p.bandwidth = getScalarField(s,"bandwidth",p.bandwidth);
    p.length = getScalarField(s,"length",p.length);
}

template <typename T>
static T getProc(HMODULE h, const char *name)
{
    FARPROC p = GetProcAddress(h, name);
    if (!p) {
        mexErrMsgIdAndTxt("h2_mod_mex:MissingSymbol", "Function '%s' was not found in h2_api.dll.", name);
    }
    return reinterpret_cast<T>(p);
}

static Api loadApi(const std::string &apiDir)
{
    Api api;
    std::string genPath = joinPath(apiDir, "genSignalWave.dll");
    std::string h2Path = joinPath(apiDir, "h2_api.dll");

    if (!fileExists(h2Path)) {
        mexErrMsgIdAndTxt("h2_mod_mex:DllNotFound", "Cannot find h2_api.dll: %s", h2Path.c_str());
    }

    SetDllDirectoryA(apiDir.c_str());

    if (fileExists(genPath)) {
        api.gen = LoadLibraryA(genPath.c_str());
        if (!api.gen) {
            mexErrMsgIdAndTxt("h2_mod_mex:DllLoadFailed", "Failed to load genSignalWave.dll from: %s. Windows error = %lu", genPath.c_str(), GetLastError());
        }
    }

    api.h2 = LoadLibraryA(h2Path.c_str());
    if (!api.h2) {
        mexErrMsgIdAndTxt("h2_mod_mex:DllLoadFailed", "Failed to load h2_api.dll from: %s. Windows error = %lu", h2Path.c_str(), GetLastError());
    }

    api.device_open_usb = getProc<device_open_usb_t>(api.h2, "device_open_usb");
    api.device_close = getProc<device_close_t>(api.h2, "device_close");

    api.mod_open = getProc<mod_open_t>(api.h2, "mod_open");
    api.mod_close = getProc<mod_close_t>(api.h2, "mod_close");

    api.mod_init_digitalmod = getProc<mod_init_digitalmod_t>(api.h2, "mod_init_digitalmod");
    api.mod_init_am = getProc<mod_init_am_t>(api.h2, "mod_init_am");
    api.mod_init_fm = getProc<mod_init_fm_t>(api.h2, "mod_init_fm");
    api.mod_init_dsss = getProc<mod_init_dsss_t>(api.h2, "mod_init_dsss");
    api.mod_init_pulse = getProc<mod_init_pulse_t>(api.h2, "mod_init_pulse");
    api.mod_init_multitone = getProc<mod_init_multitone_t>(api.h2, "mod_init_multitone");
    api.mod_init_stepsweep = getProc<mod_init_stepsweep_t>(api.h2, "mod_init_stepsweep");
    api.mod_init_rampsweep = getProc<mod_init_rampsweep_t>(api.h2, "mod_init_rampsweep");
    api.mod_init_ofdm = getProc<mod_init_ofdm_t>(api.h2, "mod_init_ofdm");
    api.mod_init_awgn = getProc<mod_init_awgn_t>(api.h2, "mod_init_awgn");

    api.mod_config_digitalmod = getProc<mod_config_digitalmod_t>(api.h2, "mod_config_digitalmod");
    api.mod_config_am = getProc<mod_config_am_t>(api.h2, "mod_config_am");
    api.mod_config_fm = getProc<mod_config_fm_t>(api.h2, "mod_config_fm");
    api.mod_config_dsss = getProc<mod_config_dsss_t>(api.h2, "mod_config_dsss");
    api.mod_config_pulse = getProc<mod_config_pulse_t>(api.h2, "mod_config_pulse");
    api.mod_config_multitone = getProc<mod_config_multitone_t>(api.h2, "mod_config_multitone");
    api.mod_config_stepsweep = getProc<mod_config_stepsweep_t>(api.h2, "mod_config_stepsweep");
    api.mod_config_rampsweep = getProc<mod_config_rampsweep_t>(api.h2, "mod_config_rampsweep");
    api.mod_config_ofdm = getProc<mod_config_ofdm_t>(api.h2, "mod_config_ofdm");
    api.mod_config_awgn = getProc<mod_config_awgn_t>(api.h2, "mod_config_awgn");

    api.mod_generate_stream = getProc<mod_generate_stream_t>(api.h2, "mod_generate_stream");
    return api;
}

static mxArray *makeInfoStruct(const std::string &modType, const std::string &version,
                               int deviceOpenStatus, int modOpenStatus, int modConfigStatus,
                               int modGenerateStatus, uint16_t channelCount, int32_t iqLength,
                               double sampleRate)
{
    std::vector<const char*> f = {
        "modType", "version", "device_open_status", "mod_open_status",
        "mod_config_status", "mod_generate_status", "channel_count",
        "iq_int16_length", "iq_points", "sample_rate"
    };
    mxArray *s = makeStruct(f);
    setFieldString(s, "modType", modType);
    setFieldString(s, "version", version);
    setFieldInt(s, "device_open_status", deviceOpenStatus);
    setFieldInt(s, "mod_open_status", modOpenStatus);
    setFieldInt(s, "mod_config_status", modConfigStatus);
    setFieldInt(s, "mod_generate_status", modGenerateStatus);
    setFieldInt(s, "channel_count", static_cast<int32_t>(channelCount));
    setFieldInt(s, "iq_int16_length", iqLength);
    setFieldDouble(s, "iq_points", static_cast<double>(iqLength) / 2.0);
    setFieldDouble(s, "sample_rate", sampleRate);
    return s;
}

static void cleanup(Api &api, void **device, bool modOpened, bool deviceOpened)
{
    if (modOpened && api.mod_close) {
        api.mod_close(device);
    }
    if (deviceOpened && api.device_close) {
        api.device_close(device);
    }
    if (api.h2) {
        FreeLibrary(api.h2);
        api.h2 = NULL;
    }
    if (api.gen) {
        FreeLibrary(api.gen);
        api.gen = NULL;
    }
    SetDllDirectoryA(NULL);
}

static mxArray *runConfig(Api &api, void **device, const std::string &modType, const mxArray *profileInMx, int &status)
{
    if (modType == "digitalmod" || modType == "digital") {
        digitalmod_profile in = {};
        digitalmod_profile out = {};
        api.mod_init_digitalmod(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_digitalmod(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "am") {
        am_profile in = {};
        am_profile out = {};
        api.mod_init_am(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_am(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "fm") {
        fm_profile in = {};
        fm_profile out = {};
        api.mod_init_fm(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_fm(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "dsss") {
        dsss_profile in = {};
        dsss_profile out = {};
        api.mod_init_dsss(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_dsss(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "pulse") {
        pulse_profile in = {};
        pulse_profile out = {};
        api.mod_init_pulse(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_pulse(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "multitone") {
        multitone_profile in = {};
        multitone_profile out = {};
        api.mod_init_multitone(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_multitone(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "stepsweep" || modType == "step_sweep") {
        stepsweep_profile in = {};
        stepsweep_profile out = {};
        api.mod_init_stepsweep(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_stepsweep(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "rampsweep" || modType == "ramp_sweep") {
        rampsweep_profile in = {};
        rampsweep_profile out = {};
        api.mod_init_rampsweep(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_rampsweep(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "ofdm") {
        ofdm_profile in = {};
        ofdm_profile out = {};
        api.mod_init_ofdm(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_ofdm(device, &in, &out);
        return profileToMx(out);
    }
    if (modType == "awgn") {
        awgn_profile in = {};
        awgn_profile out = {};
        api.mod_init_awgn(device, &in);
        applyMxToProfile(profileInMx, in);
        status = api.mod_config_awgn(device, &in, &out);
        return profileToMx(out);
    }

    mexErrMsgIdAndTxt("h2_mod_mex:UnsupportedModType",
        "Unsupported modType '%s'. Supported: digitalmod, am, fm, dsss, pulse, multitone, stepsweep, rampsweep, ofdm, awgn.",
        modType.c_str());
    return NULL;
}

static std::string versionToString(const int8_t versionBuf[50])
{
    size_t n = 0;
    while (n < 50 && versionBuf[n] != 0) ++n;
    return std::string(reinterpret_cast<const char*>(versionBuf), n);
}

static void doGenerate(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
    if (nrhs < 3) {
        mexErrMsgIdAndTxt("h2_mod_mex:Usage",
            "Usage: [iq, fs, info, profileOut] = h2_mod_mex('generate', modType, profile, symbolLength, devNum, apiDir)");
    }

    std::string modType = lowerString(mxToString(prhs[1], "modType"));
    const mxArray *profileInMx = prhs[2];

    int32_t symbolLength = 0;
    if (nrhs >= 4 && !mxIsEmpty(prhs[3])) {
        symbolLength = static_cast<int32_t>(mxGetScalar(prhs[3]));
    }

    uint8_t devNum = 0;
    if (nrhs >= 5 && !mxIsEmpty(prhs[4])) {
        devNum = static_cast<uint8_t>(mxGetScalar(prhs[4]));
    }

    std::string apiDir = ".";
    if (nrhs >= 6 && !mxIsEmpty(prhs[5])) {
        apiDir = mxToString(prhs[5], "apiDir");
    }

    Api api = loadApi(apiDir);

    void *deviceRaw = NULL;
    void **device = &deviceRaw;
    channel ch[8] = {};
    uint16_t chn = 0;
    device_info dinfo = {};

    bool deviceOpened = false;
    bool modOpened = false;

    int deviceOpenStatus = -9999;
    int modOpenStatus = -9999;
    int modConfigStatus = -9999;
    int modGenerateStatus = -9999;
    int32_t iqLength = 0;
    double sampleRate = 0.0;
    int16_t *iqPtr = NULL;
    mxArray *profileOutMx = NULL;

    int8_t versionBuf[50] = {};
    std::string version;

    deviceOpenStatus = api.device_open_usb(device, devNum, ch, &chn, &dinfo);
    if (deviceOpenStatus != 0) {
        cleanup(api, device, modOpened, deviceOpened);
        mexErrMsgIdAndTxt("h2_mod_mex:DeviceOpenFailed", "device_open_usb failed. status = %d", deviceOpenStatus);
    }
    deviceOpened = true;

    modOpenStatus = api.mod_open(device, versionBuf);
    version = versionToString(versionBuf);
    if (modOpenStatus != 0) {
        cleanup(api, device, modOpened, deviceOpened);
        mexErrMsgIdAndTxt("h2_mod_mex:ModOpenFailed", "mod_open failed. status = %d, version = '%s'", modOpenStatus, version.c_str());
    }
    modOpened = true;

    profileOutMx = runConfig(api, device, modType, profileInMx, modConfigStatus);
    if (modConfigStatus != 0) {
        cleanup(api, device, modOpened, deviceOpened);
        mexErrMsgIdAndTxt("h2_mod_mex:ModConfigFailed", "mod_config_%s failed. status = %d", modType.c_str(), modConfigStatus);
    }

    modGenerateStatus = api.mod_generate_stream(device, symbolLength, &iqPtr, &iqLength, &sampleRate);
    if (modGenerateStatus != 0) {
        cleanup(api, device, modOpened, deviceOpened);
        mexErrMsgIdAndTxt("h2_mod_mex:ModGenerateFailed", "mod_generate_stream failed. status = %d", modGenerateStatus);
    }
    if (!iqPtr || iqLength <= 0) {
        cleanup(api, device, modOpened, deviceOpened);
        mexErrMsgIdAndTxt("h2_mod_mex:InvalidIq", "mod_generate_stream returned empty IQ data. length = %d", iqLength);
    }

    plhs[0] = mxCreateNumericMatrix(static_cast<mwSize>(iqLength), 1, mxINT16_CLASS, mxREAL);
    std::memcpy(mxGetData(plhs[0]), iqPtr, static_cast<size_t>(iqLength) * sizeof(int16_t));

    if (nlhs >= 2) plhs[1] = mxCreateDoubleScalar(sampleRate);
    if (nlhs >= 3) plhs[2] = makeInfoStruct(modType, version, deviceOpenStatus, modOpenStatus, modConfigStatus, modGenerateStatus, chn, iqLength, sampleRate);
    if (nlhs >= 4) plhs[3] = profileOutMx;
    else if (profileOutMx) mxDestroyArray(profileOutMx);

    cleanup(api, device, modOpened, deviceOpened);
}

static void doHelp(int nlhs, mxArray *plhs[])
{
    const char *txt =
        "h2_mod_mex generic modulation wrapper\n"
        "\n"
        "Usage:\n"
        "  [iq, fs, info, profileOut] = h2_mod_mex('generate', modType, profile, symbolLength, devNum, apiDir)\n"
        "\n"
        "modType values:\n"
        "  digitalmod, am, fm, dsss, pulse, multitone, stepsweep, rampsweep, ofdm, awgn\n"
        "\n"
        "Examples:\n"
        "  profile = struct('rate',1000,'depth',50,'shape',1);\n"
        "  [iq,fs,info,out] = h2_mod_mex('generate','am',profile,0,0,'C:\\path\\to\\h2_api');\n";
    if (nlhs > 0) plhs[0] = mxCreateString(txt);
    else mexPrintf("%s", txt);
}

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
    if (nrhs < 1 || !mxIsChar(prhs[0])) {
        doHelp(nlhs, plhs);
        return;
    }

    std::string cmd = lowerString(mxToString(prhs[0], "command"));
    if (cmd == "help") {
        doHelp(nlhs, plhs);
        return;
    }
    if (cmd == "generate") {
        if (nlhs < 1) {
            mexErrMsgIdAndTxt("h2_mod_mex:TooFewOutputs", "At least one output is required for generate: iqData.");
        }
        if (nlhs > 4) {
            mexErrMsgIdAndTxt("h2_mod_mex:TooManyOutputs", "At most 4 outputs are supported: iq, fs, info, profileOut.");
        }
        doGenerate(nlhs, plhs, nrhs, prhs);
        return;
    }

    mexErrMsgIdAndTxt("h2_mod_mex:UnknownCommand", "Unknown command '%s'. Use 'generate' or 'help'.", cmd.c_str());
}
