#define private public
#include "emc/rs274ngc/rs274ngc_interp.hh"
#undef private

#include <cmath>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <string>
#include <vector>

#include <emscripten/emscripten.h>
#include <unistd.h>

#include "canon_event_sink.hh"
#include "emc/ini/inifile.hh"
#include "linuxcnc_hal_adapter.hh"
#include "linuxcnc_machine_config.hh"
#include "linuxcnc_runtime_state.hh"
#include "linuxcnc_tool_adapter.hh"
#include "nml_intf/emc.hh"
#include "tooldata.hh"

namespace {

void seed_hal_values()
{
    standalone::reset_hal_adapter();

    hal_data_u pin_bit{};
    pin_bit.b = true;
    standalone::set_hal_value(standalone::HalValueKind::Pin, "standalone.pin-bit", HAL_BIT,
                              pin_bit);

    hal_data_u signal_float{};
    signal_float.f = 98.25;
    standalone::set_hal_value(standalone::HalValueKind::Signal, "standalone.signal-float",
                              HAL_FLOAT, signal_float);

    hal_data_u param_s32{};
    param_s32.s = -17;
    standalone::set_hal_value(standalone::HalValueKind::Param, "standalone.param-s32", HAL_S32,
                              param_s32);

    hal_data_u pin_u32{};
    pin_u32.u = 123456789u;
    standalone::set_hal_value(standalone::HalValueKind::Pin, "standalone.pin-u32", HAL_U32,
                              pin_u32);

    hal_data_u signal_s64{};
    signal_s64.ls = -9000000000LL;
    standalone::set_hal_value(standalone::HalValueKind::Signal, "standalone.signal-s64", HAL_S64,
                              signal_s64);

    hal_data_u param_u64{};
    param_u64.lu = 9000000000ULL;
    standalone::set_hal_value(standalone::HalValueKind::Param, "standalone.param-u64", HAL_U64,
                              param_u64);

    hal_data_u disconnected{};
    disconnected.f = 12.5;
    standalone::set_hal_value(standalone::HalValueKind::Pin, "standalone.disconnected-float",
                              HAL_FLOAT, disconnected, false);
}

void seed_switchkins_hal()
{
    standalone::reset_hal_adapter();

    hal_data_u switchkins{};
    switchkins.f = 0.0;
    standalone::set_hal_value(standalone::HalValueKind::Pin, "motion.switchkins-type",
                              HAL_FLOAT, switchkins);
    standalone::set_hal_value(standalone::HalValueKind::Pin, "motion.analog-out-03",
                              HAL_FLOAT, switchkins);
    standalone::link_hal_pin_alias("motion.analog-out-03", "motion.switchkins-type");

    // Values mirror the vendored LinuxCNC xyzab-tdr sample INI/XML HAL defaults.
    const struct {
        const char *name;
        double value;
    } xyzab_tdr_defaults[] = {
        {"xyzab_tdr_kins.x-offset", -20.0},
        {"xyzab_tdr_kins.z-offset", -10.0},
        {"xyzab_tdr_kins.x-rot-point", -50.0},
        {"xyzab_tdr_kins.y-rot-point", -50.0},
        {"xyzab_tdr_kins.z-rot-point", -100.0},
    };
    for (const auto &entry : xyzab_tdr_defaults) {
        hal_data_u value{};
        value.f = entry.value;
        standalone::set_hal_value(standalone::HalValueKind::Pin, entry.name, HAL_FLOAT, value);
    }
}

void seed_tool_values()
{
    standalone::reset_tool_adapter();

    CANON_TOOL_TABLE tool = standalone::tool_entry_init();
    tool.toolno = 2;
    tool.pocketno = 2;
    tool.offset.tran.z = 1.25;
    tool.diameter = 0.25;
    standalone::set_tool_entry(2, tool);
}

void initialize_minimal_interp(Interp &interp)
{
    seed_hal_values();
    seed_tool_values();
    standalone::reset_external_axis_mask();
    interp._setup.length_units = CANON_UNITS_MM;
    interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
    interp._setup.ijk_distance_mode = DISTANCE_MODE::INCREMENTAL;
    interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE;
    interp._setup.plane = CANON_PLANE::XY;
    interp._setup.motion_mode = G_0;
    interp._setup.percent_flag = false;
    interp._setup.sequence_number = 0;
    interp._setup.parameter_occurrence = 0;
    interp._setup.num_spindles = 1;
    interp._setup.feature_set = FEATURE_INI_VARS | FEATURE_HAL_PIN_VARS;
    interp._setup.parameter_g73_peck_clearance = 1.0;
    interp._setup.parameter_g83_peck_clearance = 1.0;
    interp._setup.parameters[5599] = 1.0;
    interp.load_tool_table();
    standalone::apply_axis_mask_to_interp(interp, standalone::axis_mask_from_coordinates("XYZABCUVW"));
    standalone::reset_user_m_code_registry();
}

void initialize_fiveaxis_remap_interp(Interp &interp)
{
    seed_switchkins_hal();
    seed_tool_values();
    standalone::reset_external_axis_mask();
    interp._setup.length_units = CANON_UNITS_MM;
    interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
    interp._setup.ijk_distance_mode = DISTANCE_MODE::INCREMENTAL;
    interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE;
    interp._setup.plane = CANON_PLANE::XY;
    interp._setup.motion_mode = G_0;
    interp._setup.percent_flag = false;
    interp._setup.sequence_number = 0;
    interp._setup.parameter_occurrence = 0;
    interp._setup.num_spindles = 1;
    interp._setup.feature_set = FEATURE_INI_VARS | FEATURE_HAL_PIN_VARS;
    interp._setup.parameter_g73_peck_clearance = 1.0;
    interp._setup.parameter_g83_peck_clearance = 1.0;
    interp._setup.parameters[5599] = 1.0;
    interp.load_tool_table();
    standalone::apply_axis_mask_to_interp(interp, standalone::axis_mask_from_coordinates("XYZABCUVW"));
    standalone::reset_user_m_code_registry();
    interp.init_named_parameters();
}

void initialize_indexer_interp(Interp &interp)
{
    standalone::reset_hal_adapter();
    standalone::reset_tool_adapter();

    interp._setup.length_units = CANON_UNITS_MM;
    interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
    interp._setup.ijk_distance_mode = DISTANCE_MODE::ABSOLUTE;
    interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE;
    interp._setup.plane = CANON_PLANE::XY;
    interp._setup.motion_mode = G_0;
    interp._setup.percent_flag = false;
    interp._setup.sequence_number = 1;
    interp._setup.parameter_occurrence = 0;
    interp._setup.num_spindles = 1;
    interp._setup.a_indexer_jnum = 0;
    interp._setup.parameters[5599] = 1.0;
    std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
}

std::vector<std::string> split_program_lines(const char *program_text)
{
    std::vector<std::string> lines;
    std::istringstream input(program_text ? program_text : "");
    std::string line;
    while (std::getline(input, line)) {
        if (!line.empty()) {
            lines.push_back(line + "\n");
        }
    }
    return lines;
}

char *copy_result(const std::string &result)
{
    char *out = static_cast<char *>(std::malloc(result.size() + 1));
    if (!out) {
        return nullptr;
    }
    std::memcpy(out, result.c_str(), result.size() + 1);
    return out;
}

void append_events_and_state(std::ostringstream &output, const Interp &interp)
{
    for (const auto &event : standalone::canon_events()) {
        output << "canon_event=" << event << "\n";
    }
    output << "setup.current_x=" << interp._setup.current_x << "\n";
    output << "setup.current_y=" << interp._setup.current_y << "\n";
    output << "setup.current_z=" << interp._setup.current_z << "\n";
    output << "setup.parameter_5420=" << interp._setup.parameters[5420] << "\n";
    output << "setup.parameter_5421=" << interp._setup.parameters[5421] << "\n";
    output << "setup.parameter_5422=" << interp._setup.parameters[5422] << "\n";
    output << "post_execute.plane=" << static_cast<int>(interp._setup.plane) << "\n";
    output << "post_execute.distance_mode=" << static_cast<int>(interp._setup.distance_mode) << "\n";
    output << "post_execute.feed_mode=" << static_cast<int>(interp._setup.feed_mode) << "\n";
    output << "post_execute.motion_mode=" << interp._setup.motion_mode << "\n";
    output << "post_execute.origin_index=" << interp._setup.origin_index << "\n";
    output << "post_execute.feed_override=" << interp._setup.feed_override << "\n";
    output << "post_execute.speed_override_0=" << interp._setup.speed_override[0] << "\n";
    output << "post_execute.spindle_turning_0=" << interp._setup.spindle_turning[0] << "\n";
    output << "post_execute.mist=" << interp._setup.mist << "\n";
    output << "post_execute.flood=" << interp._setup.flood << "\n";
}

void append_parameter_state(std::ostringstream &output, const Interp &interp)
{
    output << "parameter_5161=" << interp._setup.parameters[5161] << "\n";
    output << "parameter_5162=" << interp._setup.parameters[5162] << "\n";
    output << "parameter_5220=" << interp._setup.parameters[5220] << "\n";
    output << "parameter_5221=" << interp._setup.parameters[5221] << "\n";
    output << "parameter_5399=" << interp._setup.parameters[5399] << "\n";
}

void append_tool_entry(std::ostringstream &output, const char *prefix, const CANON_TOOL_TABLE &tool)
{
    output << prefix << ".toolno=" << tool.toolno << "\n";
    output << prefix << ".pocketno=" << tool.pocketno << "\n";
    output << prefix << ".z=" << tool.offset.tran.z << "\n";
    output << prefix << ".diameter=" << tool.diameter << "\n";
    output << prefix << ".frontangle=" << tool.frontangle << "\n";
    output << prefix << ".backangle=" << tool.backangle << "\n";
    output << prefix << ".orientation=" << tool.orientation << "\n";
    output << prefix << ".comment=" << tool.comment << "\n";
}

void append_tool_table_state(std::ostringstream &output)
{
    output << "tooldata_last_index=" << tooldata_last_index_get() << "\n";

    CANON_TOOL_TABLE tool = standalone::tool_entry_init();
    if (standalone::get_tool_entry(&tool, 0) == 0) {
        append_tool_entry(output, "tool_0", tool);
    }
    if (standalone::get_tool_entry(&tool, 1) == 0) {
        append_tool_entry(output, "tool_1", tool);
    }
    if (standalone::get_tool_entry(&tool, 2) == 0) {
        append_tool_entry(output, "tool_2", tool);
    }
    for (int idx = 0; idx < CANON_POCKETS_MAX; ++idx) {
        if (standalone::get_tool_entry(&tool, idx) == 0 && tool.toolno != -1) {
            std::ostringstream prefix;
            prefix << "tool_pocket_" << idx;
            append_tool_entry(output, prefix.str().c_str(), tool);
        }
    }
    output << "tool_index_for_tool_2=" << standalone::find_tool_index_for_tool(2) << "\n";
}

bool is_uri_unreserved(char value)
{
    return (value >= 'A' && value <= 'Z') ||
           (value >= 'a' && value <= 'z') ||
           (value >= '0' && value <= '9') ||
           value == '-' || value == '_' || value == '.' || value == '~';
}

void append_percent_encoded_text(std::ostringstream &output, const char *text)
{
    constexpr char hex[] = "0123456789ABCDEF";
    for (const char *cursor = text ? text : ""; *cursor; ++cursor) {
        char value = *cursor;
        switch (*cursor) {
        case '\r':
        case '\n':
        case '\t':
            value = ' ';
            break;
        default:
            break;
        }
        if (is_uri_unreserved(value)) {
            output << value;
        } else {
            const auto byte = static_cast<unsigned char>(value);
            output << '%' << hex[(byte >> 4) & 0x0f] << hex[byte & 0x0f];
        }
    }
}

void append_run_step(std::ostringstream &output,
                     const Interp &interp,
                     const char *phase,
                     int step,
                     int rc)
{
    output << "run_step phase=" << phase
           << " step=" << step
           << " rc=" << rc
           << " line=" << interp._setup.sequence_number
           << " x=" << interp._setup.current_x
           << " y=" << interp._setup.current_y
           << " z=" << interp._setup.current_z
           << " a=" << interp._setup.AA_current
           << " b=" << interp._setup.BB_current
           << " c=" << interp._setup.CC_current
           << " u=" << interp._setup.u_current
           << " v=" << interp._setup.v_current
           << " w=" << interp._setup.w_current
           << " statement_uri=";
    append_percent_encoded_text(output, interp._setup.linetext);
    output << "\n";
}

void append_error_text(std::ostringstream &output, Interp &interp, const char *prefix, int rc)
{
    if (rc > INTERP_MIN_ERROR) {
        char error_buf[LINELEN] = {0};
        interp.error_text(rc, error_buf, sizeof(error_buf));
        output << prefix << error_buf << "\n";
    }
}

void append_named_value(std::ostringstream &output, Interp &interp, const char *name)
{
    int status = 0;
    double value = 0.0;
    const int rc = interp.find_named_param(name, &status, &value);
    output << name << ": rc=" << rc << " found=" << status << " value=" << value << "\n";
}

std::string command_label(const char *command)
{
    std::string value = command ? command : "";
    while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) {
        value.pop_back();
    }
    return value;
}

std::string parent_path(const char *path)
{
    std::string value = path ? path : "";
    const std::string::size_type slash = value.find_last_of('/');
    if (slash == std::string::npos) {
        return ".";
    }
    if (slash == 0) {
        return "/";
    }
    return value.substr(0, slash);
}

bool parse_machine_remaps(Interp &interp, std::ostringstream &output, const char *ini_path)
{
    const std::string machine_dir = parent_path(ini_path);
    const std::string remap_dir = machine_dir + "/remap_subs";

    linuxcnc::IniFile ini(ini_path ? ini_path : "");
    const bool opened = static_cast<bool>(ini);
    output << "fiveaxis_ini_open=" << (opened ? 1 : 0) << "\n";
    if (!opened) {
        return false;
    }

    std::snprintf(interp._setup.program_prefix, sizeof(interp._setup.program_prefix), "%s",
                  machine_dir.c_str());
    interp._setup.subroutines[0] = strstore(remap_dir.c_str());

    bool ok = true;
    if (auto tool_table = ini.findString("TOOL_TABLE", "EMCIO")) {
        const std::string tool_path = machine_dir + "/" + *tool_table;
        tooldata_init(false);
        tooldata_set_db(DB_NOTUSED);
        const int tool_rc = tooldata_load(tool_path.c_str());
        output << "fiveaxis_tool_table_load=" << tool_rc << "\n";
        ok &= tool_rc == 0;
        ok &= interp.load_tool_table() == INTERP_OK;
    }

    int remap_count = 0;
    while (auto remap = ini.findString(++remap_count, "REMAP", "RS274NGC")) {
        const int line_number = ini.lineOf(remap_count, "REMAP", "RS274NGC").second;
        const int rc = interp.parse_remap(remap->c_str(), line_number);
        output << "fiveaxis_parse_remap_" << remap_count << "=" << rc << "\n";
        ok &= rc == INTERP_OK;
    }

    remap_count -= 1;
    output << "fiveaxis_parsed_remap_count=" << remap_count << "\n";
    ok &= remap_count == 2 || remap_count == 3;
    output << "fiveaxis_remaps_ready=" << (ok ? 1 : 0) << "\n";
    return ok;
}

std::string remap_subroutine_path(const std::string &machine_dir, const std::string &entry)
{
    if (!entry.empty() && entry[0] == '/') {
        return entry;
    }
    if (machine_dir == "/") {
        return machine_dir + entry;
    }
    return machine_dir + "/" + entry;
}

std::string resolve_machine_relative_path(const std::string &machine_dir, const std::string &entry)
{
    if (entry.empty() || entry[0] == '/') {
        return entry;
    }
    if (machine_dir == "/") {
        return machine_dir + entry;
    }
    return machine_dir + "/" + entry;
}

int apply_ini_search_paths(Interp &interp, const linuxcnc::IniFile &ini, const std::string &machine_dir)
{
    standalone::apply_machine_axis_config(interp, ini);
    standalone::load_tool_table_from_ini(interp, ini, machine_dir.c_str());
    standalone::register_user_m_codes_from_ini(ini, machine_dir.c_str());

    std::snprintf(interp._setup.program_prefix, sizeof(interp._setup.program_prefix), "%s",
                  machine_dir.c_str());

    int subroutine_count = 0;
    if (auto subroutine_path_text = ini.findString("SUBROUTINE_PATH", "RS274NGC")) {
        std::istringstream paths(*subroutine_path_text);
        std::string entry;
        while (std::getline(paths, entry, ':') && subroutine_count < MAX_SUB_DIRS) {
            const std::string resolved = remap_subroutine_path(machine_dir, entry);
            interp._setup.subroutines[subroutine_count++] = strstore(resolved.c_str());
        }
    }

    if (ini.findBoolV("OWORD_NARGS", "RS274NGC", false)) {
        interp._setup.feature_set |= FEATURE_OWORD_N_ARGS;
    }

    return subroutine_count;
}

bool apply_ini_search_paths(Interp &interp, const char *ini_path)
{
    if (!ini_path || ini_path[0] == '\0') {
        return true;
    }

    linuxcnc::IniFile ini(ini_path);
    if (!ini) {
        return false;
    }

    apply_ini_search_paths(interp, ini, parent_path(ini_path));
    return true;
}

int initialize_parameter_file_from_ini(Interp &interp, const char *ini_path)
{
    standalone::reset_parameter_file_name();
    if (!ini_path || ini_path[0] == '\0') {
        return INTERP_OK;
    }

    linuxcnc::IniFile ini(ini_path);
    if (!ini || !ini.findString("PARAMETER_FILE", "RS274NGC")) {
        return INTERP_OK;
    }

    const int ini_rc = interp.ini_load(ini_path);
    if (ini_rc != INTERP_OK) {
        return ini_rc;
    }

    char parameter_file_name[LINELEN] = {0};
    standalone::get_parameter_file_name(parameter_file_name, sizeof(parameter_file_name));
    const std::string resolved =
        resolve_machine_relative_path(parent_path(ini_path), parameter_file_name);
    if (access(resolved.c_str(), F_OK) != 0) {
        standalone::reset_parameter_file_name();
        return INTERP_OK;
    }
    standalone::set_parameter_file_name(resolved.c_str());

    return interp.init();
}

bool parse_remaps_from_ini(Interp &interp, std::ostringstream &output, const char *ini_path)
{
    const std::string machine_dir = parent_path(ini_path);

    linuxcnc::IniFile ini(ini_path ? ini_path : "");
    const bool opened = static_cast<bool>(ini);
    output << "remap_ini_open=" << (opened ? 1 : 0) << "\n";
    if (!opened) {
        return false;
    }

    const int subroutine_count = apply_ini_search_paths(interp, ini, machine_dir);
    output << "remap_subroutine_path_count=" << subroutine_count << "\n";

    bool ok = subroutine_count > 0;
    int remap_count = 0;
    while (auto remap = ini.findString(++remap_count, "REMAP", "RS274NGC")) {
        const int line_number = ini.lineOf(remap_count, "REMAP", "RS274NGC").second;
        const int rc = interp.parse_remap(remap->c_str(), line_number);
        output << "remap_parse_remap_" << remap_count << "=" << rc << "\n";
        ok &= rc == INTERP_OK;
    }

    remap_count -= 1;
    output << "remap_parsed_remap_count=" << remap_count << "\n";
    ok &= remap_count > 0;
    output << "remap_remaps_ready=" << (ok ? 1 : 0) << "\n";
    return ok;
}

void apply_parameter_assignments(Interp &interp, const char *assignments)
{
    std::istringstream input(assignments ? assignments : "");
    int parameter = 0;
    double value = 0.0;
    while (input >> parameter >> value) {
        if ((parameter > 0) && (parameter < interp_param_global::RS274NGC_MAX_PARAMETERS)) {
            interp._setup.parameters[parameter] = value;
        }
    }
}

char *load_tool_table_with_mode(const char *path, bool random_tool_changer)
{
    standalone::reset_tool_adapter();
    tooldata_init(random_tool_changer);
    tooldata_set_db(DB_NOTUSED);

    std::ostringstream output;
    output << "tooldata_random_toolchanger=" << (random_tool_changer ? 1 : 0) << "\n";
    const int rc = tooldata_load(path);
    output << "tooldata_load=" << rc << "\n";
    append_tool_table_state(output);
    return copy_result(output.str());
}

}  // namespace

extern "C" {

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_program_with_ini(const char *program_text, const char *ini_path)
{
    if (ini_path && ini_path[0] != '\0') {
        setenv("INI_FILE_NAME", ini_path, 1);
    } else {
        unsetenv("INI_FILE_NAME");
    }

    Interp interp;
    standalone::reset_canon_events();
    initialize_minimal_interp(interp);
    apply_ini_search_paths(interp, ini_path);

    const std::vector<std::string> program = split_program_lines(program_text);
    std::ostringstream output;
    if (program.empty()) {
        output << "error=empty fixture\n";
        return copy_result(output.str());
    }

    for (std::size_t idx = 0; idx < program.size(); ++idx) {
        interp._setup.sequence_number = static_cast<int>(idx + 1);
        const int execute_rc = interp.execute(program[idx].c_str());
        output << "execute_line_" << (idx + 1) << "=" << execute_rc << "\n";
        if (execute_rc > INTERP_MIN_ERROR) {
            char error_buf[LINELEN] = {0};
            interp.error_text(execute_rc, error_buf, sizeof(error_buf));
            output << "error_text=" << error_buf << "\n";
            break;
        }
    }

    append_events_and_state(output, interp);
    return copy_result(output.str());
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_program(const char *program_text)
{
    return lcinterp_run_program_with_ini(program_text, nullptr);
}

char *run_file_with_ini_internal(const char *path, const char *ini_path, bool continue_on_error)
{
    if (ini_path && ini_path[0] != '\0') {
        setenv("INI_FILE_NAME", ini_path, 1);
    } else {
        unsetenv("INI_FILE_NAME");
    }

    Interp interp;
    standalone::reset_canon_events();
    initialize_minimal_interp(interp);

    std::ostringstream output;
    apply_ini_search_paths(interp, ini_path);
    const int init_rc = initialize_parameter_file_from_ini(interp, ini_path);
    if (init_rc != INTERP_OK) {
        output << "file_init=" << init_rc << "\n";
        append_error_text(output, interp, "file_error_text=", init_rc);
        append_events_and_state(output, interp);
        return copy_result(output.str());
    }
    apply_ini_search_paths(interp, ini_path);

    const int open_rc = interp.open(path);
    output << "file_open=" << open_rc << "\n";
    if (open_rc != INTERP_OK) {
        if (open_rc > INTERP_MIN_ERROR) {
            char error_buf[LINELEN] = {0};
            interp.error_text(open_rc, error_buf, sizeof(error_buf));
            output << "file_error_text=" << error_buf << "\n";
        }
        append_events_and_state(output, interp);
        return copy_result(output.str());
    }

    int file_read_count = 0;
    int file_execute_count = 0;
    bool saw_error = false;
    while (true) {
        const int read_rc = interp.read();
        if (read_rc == INTERP_ENDFILE) {
            output << "file_read_eof=" << read_rc << "\n";
            break;
        }

        ++file_read_count;
        output << "file_read_" << file_read_count << "=" << read_rc << "\n";
        append_run_step(output, interp, "read", file_read_count, read_rc);
        if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
            if (read_rc > INTERP_MIN_ERROR) {
                char error_buf[LINELEN] = {0};
                interp.error_text(read_rc, error_buf, sizeof(error_buf));
                output << "file_error_text=" << error_buf << "\n";
            }
            saw_error = true;
            if (!continue_on_error) {
                break;
            }
            continue;
        }

        const int execute_rc = interp.execute();
        ++file_execute_count;
        output << "file_execute_" << file_execute_count << "=" << execute_rc << "\n";
        append_run_step(output, interp, "execute", file_execute_count, execute_rc);
        if (execute_rc > INTERP_MIN_ERROR) {
            char error_buf[LINELEN] = {0};
            interp.error_text(execute_rc, error_buf, sizeof(error_buf));
            output << "file_error_text=" << error_buf << "\n";
        }
        if ((execute_rc != INTERP_OK) &&
            (execute_rc != INTERP_EXECUTE_FINISH) &&
            (execute_rc != INTERP_EXIT)) {
            saw_error = true;
            break;
        }
        if (execute_rc == INTERP_EXIT) {
            break;
        }
    }

    output << "file_read_count=" << file_read_count << "\n";
    output << "file_execute_count=" << file_execute_count << "\n";
    output << "file_saw_error=" << (saw_error ? 1 : 0) << "\n";
    append_events_and_state(output, interp);
    return copy_result(output.str());
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
{
    return run_file_with_ini_internal(path, ini_path, false);
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file_with_ini_continue_on_error(const char *path, const char *ini_path)
{
    return run_file_with_ini_internal(path, ini_path, true);
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file(const char *path)
{
    return lcinterp_run_file_with_ini(path, nullptr);
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_fiveaxis_remap_file(const char *path, const char *ini_path)
{
    if (ini_path && ini_path[0] != '\0') {
        setenv("INI_FILE_NAME", ini_path, 1);
    } else {
        unsetenv("INI_FILE_NAME");
    }

    Interp interp;
    standalone::reset_canon_events();
    initialize_fiveaxis_remap_interp(interp);

    std::ostringstream output;
    bool ok = parse_machine_remaps(interp, output, ini_path);

    const int open_rc = interp.open(path);
    output << "fiveaxis_file_open=" << open_rc << "\n";
    if (open_rc != INTERP_OK) {
        append_error_text(output, interp, "fiveaxis_file_error_text=", open_rc);
        output << "fiveaxis_linuxcnc_remap_file_execute=0\n";
        unsetenv("INI_FILE_NAME");
        return copy_result(output.str());
    }

    constexpr int max_file_steps = 8192;
    int file_read_count = 0;
    int file_execute_count = 0;
    int file_finish_count = 0;
    int final_rc = INTERP_OK;
    bool reached_exit = false;

    while ((file_read_count < max_file_steps) && (file_execute_count < max_file_steps)) {
        const int read_rc = interp.read();
        if (read_rc == INTERP_ENDFILE) {
            output << "fiveaxis_file_read_eof=" << read_rc << "\n";
            final_rc = read_rc;
            break;
        }

        ++file_read_count;
        if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
            output << "fiveaxis_file_read_" << file_read_count << "=" << read_rc << "\n";
            append_error_text(output, interp, "fiveaxis_file_error_text=", read_rc);
            final_rc = read_rc;
            ok = false;
            break;
        }

        const int execute_rc = interp.execute();
        ++file_execute_count;
        final_rc = execute_rc;
        if (execute_rc == INTERP_EXECUTE_FINISH) {
            ++file_finish_count;
        }

        if ((execute_rc != INTERP_OK) && (execute_rc != INTERP_EXIT) &&
            (execute_rc != INTERP_EXECUTE_FINISH)) {
            output << "fiveaxis_file_execute_" << file_execute_count << "="
                   << execute_rc << "\n";
            append_error_text(output, interp, "fiveaxis_file_error_text=", execute_rc);
            ok = false;
            break;
        }

        if (execute_rc == INTERP_EXIT) {
            reached_exit = true;
            break;
        }
    }

    int hal_status = 0;
    double switchkins_type = 0.0;
    const int hal_rc =
        interp.find_named_param("_hal[motion.switchkins-type]", &hal_status, &switchkins_type);

    output << "fiveaxis_file_read_count=" << file_read_count << "\n";
    output << "fiveaxis_file_execute_count=" << file_execute_count << "\n";
    output << "fiveaxis_file_finish_count=" << file_finish_count << "\n";
    output << "fiveaxis_file_final_rc=" << final_rc << "\n";
    output << "fiveaxis_file_reached_exit=" << (reached_exit ? 1 : 0) << "\n";
    output << "fiveaxis_hal_switchkins: rc=" << hal_rc << " found=" << hal_status
           << " value=" << switchkins_type << "\n";

    ok &= reached_exit;
    ok &= file_read_count > 0;
    ok &= file_execute_count > 0;
    ok &= file_read_count < max_file_steps;
    ok &= file_execute_count < max_file_steps;
    ok &= hal_rc == INTERP_OK && hal_status == 1;

    output << "fiveaxis_linuxcnc_remap_file_execute=" << (ok ? 1 : 0) << "\n";
    unsetenv("INI_FILE_NAME");
    return copy_result(output.str());
}

char *run_remap_file_internal(const char *path,
                              const char *ini_path,
                              bool continue_on_error,
                              bool expect_error)
{
    if (ini_path && ini_path[0] != '\0') {
        setenv("INI_FILE_NAME", ini_path, 1);
    } else {
        unsetenv("INI_FILE_NAME");
    }

    Interp interp;
    standalone::reset_canon_events();
    initialize_minimal_interp(interp);
    interp.init_named_parameters();

    std::ostringstream output;
    bool ok = parse_remaps_from_ini(interp, output, ini_path);

    const int open_rc = interp.open(path);
    output << "remap_file_open=" << open_rc << "\n";
    if (open_rc != INTERP_OK) {
        append_error_text(output, interp, "remap_file_error_text=", open_rc);
        output << "remap_linuxcnc_file_execute=0\n";
        unsetenv("INI_FILE_NAME");
        return copy_result(output.str());
    }

    constexpr int max_file_steps = 20000;
    int file_read_count = 0;
    int file_execute_count = 0;
    int file_finish_count = 0;
    int final_rc = INTERP_OK;
    bool reached_exit = false;
    bool reached_endfile = false;
    bool saw_error = false;

    while ((file_read_count < max_file_steps) && (file_execute_count < max_file_steps)) {
        const int read_rc = interp.read();
        if (read_rc == INTERP_ENDFILE) {
            output << "remap_file_read_eof=" << read_rc << "\n";
            final_rc = read_rc;
            reached_endfile = true;
            break;
        }

        ++file_read_count;
        if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
            output << "remap_file_read_" << file_read_count << "=" << read_rc << "\n";
            append_error_text(output, interp, "remap_file_error_text=", read_rc);
            final_rc = read_rc;
            saw_error = true;
            if (!expect_error) {
                ok = false;
            }
            if (!continue_on_error) {
                break;
            }
            continue;
        }

        const int execute_rc = interp.execute();
        ++file_execute_count;
        final_rc = execute_rc;
        if (execute_rc == INTERP_EXECUTE_FINISH) {
            ++file_finish_count;
        }
        if ((execute_rc != INTERP_OK) && (execute_rc != INTERP_EXIT) &&
            (execute_rc != INTERP_EXECUTE_FINISH)) {
            output << "remap_file_execute_" << file_execute_count << "=" << execute_rc << "\n";
            append_error_text(output, interp, "remap_file_error_text=", execute_rc);
            saw_error = true;
            if (!expect_error) {
                ok = false;
            }
            if (!continue_on_error) {
                break;
            }
            continue;
        }

        if (execute_rc == INTERP_EXIT) {
            reached_exit = true;
            break;
        }
    }

    output << "remap_file_read_count=" << file_read_count << "\n";
    output << "remap_file_execute_count=" << file_execute_count << "\n";
    output << "remap_file_finish_count=" << file_finish_count << "\n";
    output << "remap_file_final_rc=" << final_rc << "\n";
    output << "remap_file_reached_exit=" << (reached_exit ? 1 : 0) << "\n";
    output << "remap_file_reached_endfile=" << (reached_endfile ? 1 : 0) << "\n";
    output << "remap_file_saw_error=" << (saw_error ? 1 : 0) << "\n";
    for (const auto &event : standalone::canon_events()) {
        output << "remap_canon_event=" << event << "\n";
    }

    ok &= file_read_count > 0;
    ok &= file_execute_count > 0;
    ok &= file_read_count < max_file_steps;
    ok &= file_execute_count < max_file_steps;
    ok &= reached_exit || reached_endfile;
    ok &= expect_error ? saw_error : !saw_error;

    output << "remap_linuxcnc_file_execute=" << (ok ? 1 : 0) << "\n";
    unsetenv("INI_FILE_NAME");
    return copy_result(output.str());
}

}  // extern "C"

bool execute_remap_mdi_command(Interp &interp, std::ostringstream &output, const char *command)
{
    const std::string label = command_label(command);
    int rc = interp.execute(command);
    output << "remap_mdi_" << label << "_execute_0=" << rc << "\n";

    int finish_count = 0;
    while (rc == INTERP_EXECUTE_FINISH && finish_count < 16) {
        ++finish_count;
        rc = interp.execute();
        output << "remap_mdi_" << label << "_execute_" << finish_count << "=" << rc << "\n";
    }

    output << "remap_mdi_" << label << "_execute_finish_count=" << finish_count << "\n";
    return rc == INTERP_OK;
}

char *run_remap_io_mdi_sequence_internal(const char *ini_path)
{
    if (ini_path && ini_path[0] != '\0') {
        setenv("INI_FILE_NAME", ini_path, 1);
    } else {
        unsetenv("INI_FILE_NAME");
    }

    Interp interp;
    standalone::reset_canon_events();
    initialize_minimal_interp(interp);
    interp.init_named_parameters();

    std::ostringstream output;
    bool ok = parse_remaps_from_ini(interp, output, ini_path);
    ok &= output.str().find("remap_parsed_remap_count=7") != std::string::npos;

    const struct MdiStep {
        const char *command;
        bool digital_input;
        int digital_value;
        bool analog_input;
        double analog_value;
    } steps[] = {
        {"M62 P1\n", false, 0, false, 0.0},
        {"G0 X1.0\n", false, 0, false, 0.0},
        {"M63 P1\n", false, 0, false, 0.0},
        {"G0 X2.0\n", false, 0, false, 0.0},
        {"M62 P1\n", false, 0, false, 0.0},
        {"G0 X3.0\n", false, 0, false, 0.0},
        {"M63 P1\n", false, 0, false, 0.0},
        {"G0 X4.0\n", false, 0, false, 0.0},
        {"M64 P1\n", false, 0, false, 0.0},
        {"M65 P1\n", false, 0, false, 0.0},
        {"M64 P1\n", false, 0, false, 0.0},
        {"M65 P1\n", false, 0, false, 0.0},
        {"M66 P1\n", true, 1, false, 0.0},
        {"M66 P1\n", true, 0, false, 0.0},
        {"M66 E1 L0\n", false, 0, true, 42.13},
        {"M66 E1 L0\n", false, 0, true, -13.42},
        {"M67 E1 Q42.13\n", false, 0, false, 0.0},
        {"G0 X-1.0\n", false, 0, false, 0.0},
        {"M67 E1 Q-13.42\n", false, 0, false, 0.0},
        {"G0 X-2.0\n", false, 0, false, 0.0},
        {"M67 E1 Q0.0\n", false, 0, false, 0.0},
        {"G0 X-3.0\n", false, 0, false, 0.0},
        {"M68 E1 Q42.13\n", false, 0, false, 0.0},
        {"M68 E1 Q-13.42\n", false, 0, false, 0.0},
        {"M68 E1 Q0.0\n", false, 0, false, 0.0},
    };

    for (const auto &step : steps) {
        if (step.digital_input) {
            standalone::set_external_digital_input(0, step.digital_value);
        }
        if (step.analog_input) {
            standalone::set_external_analog_input(0, step.analog_value);
        }
        ok &= execute_remap_mdi_command(interp, output, step.command);
        output << "remap_mdi_parameter_5399=" << interp._setup.parameters[5399] << "\n";
        output << "remap_mdi_parameter_100=" << interp._setup.parameters[100] << "\n";
    }

    output << "remap_mdi_canon_event_count=" << standalone::canon_events().size() << "\n";
    for (const auto &event : standalone::canon_events()) {
        output << "remap_mdi_canon_event=" << event << "\n";
    }

    ok &= std::fabs(interp._setup.parameters[5399] - -13.42) < 1e-9;
    ok &= std::fabs(interp._setup.parameters[100] - -13.42) < 1e-9;
    output << "remap_io_ngc_linuxcnc_mdi_sequence=" << (ok ? 1 : 0) << "\n";
    unsetenv("INI_FILE_NAME");
    return copy_result(output.str());
}

extern "C" {

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_remap_file(const char *path, const char *ini_path)
{
    return run_remap_file_internal(path, ini_path, false, false);
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_remap_file_continue_on_error(const char *path, const char *ini_path)
{
    return run_remap_file_internal(path, ini_path, true, true);
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_remap_io_mdi_sequence(const char *ini_path)
{
    return run_remap_io_mdi_sequence_internal(ini_path);
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_restore_parameters(const char *path)
{
    Interp interp;
    initialize_minimal_interp(interp);

    std::ostringstream output;
    const int rc = interp.restore_parameters(path);
    output << "restore_parameters=" << rc << "\n";
    append_error_text(output, interp, "restore_error_text=", rc);
    append_parameter_state(output, interp);
    return copy_result(output.str());
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_save_parameters(const char *path, const char *assignments)
{
    Interp interp;
    initialize_minimal_interp(interp);
    apply_parameter_assignments(interp, assignments);

    std::ostringstream output;
    const int rc = interp.save_parameters(path, interp._setup.parameters);
    output << "save_parameters=" << rc << "\n";
    append_error_text(output, interp, "save_error_text=", rc);
    append_parameter_state(output, interp);
    return copy_result(output.str());
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_load_tool_table(const char *path)
{
    return load_tool_table_with_mode(path, false);
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_load_tool_table_random(const char *path)
{
    return load_tool_table_with_mode(path, true);
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_save_tool_table(const char *path)
{
    std::ostringstream output;
    const int rc = tooldata_save(path);
    output << "tooldata_save=" << rc << "\n";
    append_tool_table_state(output);
    return copy_result(output.str());
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_probe_init_and_synch()
{
    std::ostringstream output;

    emcStatus->motion.traj.linearUnits = 1.0;
    standalone::reset_tool_adapter();
    Interp interp;
    standalone::reset_canon_events();
    const int rc = interp.init();
    output << "init=" << rc << "\n";
    output << "setup.length_units=" << static_cast<int>(interp._setup.length_units) << "\n";
    output << "setup.origin_index=" << interp._setup.origin_index << "\n";
    output << "setup.distance_mode=" << static_cast<int>(interp._setup.distance_mode) << "\n";
    output << "setup.feed_mode=" << static_cast<int>(interp._setup.feed_mode) << "\n";
    output << "setup.motion_mode=" << interp._setup.motion_mode << "\n";
    output << "canon_event_count=" << standalone::canon_events().size() << "\n";
    for (const auto &event : standalone::canon_events()) {
        output << "canon_event=" << event << "\n";
    }

    emcStatus->motion.traj.linearUnits = 1.0 / 25.4;
    standalone::reset_tool_adapter();
    Interp inch_interp;
    standalone::reset_canon_events();
    const int inch_rc = inch_interp.init();
    output << "inch_init=" << inch_rc << "\n";
    output << "inch_setup.length_units="
           << static_cast<int>(inch_interp._setup.length_units) << "\n";
    output << "inch_external_length_units=" << GET_EXTERNAL_LENGTH_UNITS() << "\n";
    output << "inch_canon_event_count=" << standalone::canon_events().size() << "\n";
    for (const auto &event : standalone::canon_events()) {
        output << "inch_canon_event=" << event << "\n";
    }

    standalone::reset_tool_adapter();
    CANON_TOOL_TABLE tool = standalone::tool_entry_init();
    tool.toolno = 2;
    tool.pocketno = 2;
    standalone::set_tool_entry(2, tool);
    standalone::select_tool(2);
    standalone::change_selected_tool();

    Interp synch_interp;
    const int synch_rc = synch_interp.synch();
    output << "synch=" << synch_rc << "\n";
    output << "synch.current_pocket=" << synch_interp._setup.current_pocket << "\n";
    output << "synch.selected_pocket=" << synch_interp._setup.selected_pocket << "\n";
    output << "synch.tool_0=" << synch_interp._setup.tool_table[0].toolno << "\n";
    output << "synch.tool_2=" << synch_interp._setup.tool_table[2].toolno << "\n";

    emcStatus->motion.traj.linearUnits = 1.0;
    standalone::reset_tool_adapter();
    return copy_result(output.str());
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_probe_indexer()
{
    Interp interp;
    standalone::reset_canon_events();
    initialize_indexer_interp(interp);

    std::ostringstream output;
    const int rc = interp.execute("G0 A90\n");
    output << "execute=" << rc << "\n";
    output << "post_a=" << interp._setup.AA_current << "\n";
    output << "canon_event_count=" << standalone::canon_events().size() << "\n";
    for (const auto &event : standalone::canon_events()) {
        output << "canon_event=" << event << "\n";
    }
    return copy_result(output.str());
}

EMSCRIPTEN_KEEPALIVE
char *lcinterp_probe_named_parameters(const char *ini_path)
{
    if (ini_path && ini_path[0] != '\0') {
        setenv("INI_FILE_NAME", ini_path, 1);
    } else {
        unsetenv("INI_FILE_NAME");
    }

    Interp interp;
    seed_hal_values();
    interp._setup.feature_set = FEATURE_INI_VARS | FEATURE_HAL_PIN_VARS;
    interp._setup.length_units = CANON_UNITS_MM;
    interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
    interp._setup.motion_mode = G_1;
    interp._setup.feed_rate = 123.45;
    interp._setup.speed[0] = 678.9;
    interp._setup.current_x = 1.25;
    interp._setup.current_y = 2.5;
    interp._setup.current_z = 3.75;
    interp._setup.parameters[interp_param_global::TOOL_NUMBER] = 12.0;

    std::ostringstream output;
    const int rc = interp.init_named_parameters();
    output << "init_named_parameters=" << rc << "\n";
    output << "global_named_count=" << interp._setup.sub_context[0].named_params.size() << "\n";
    output << "required_parameter_first=" << interp_param_global::G28_X << "\n";
    output << "readonly_tool_number_index=" << interp_param_global::TOOL_NUMBER << "\n";

    append_named_value(output, interp, "_vmajor");
    append_named_value(output, interp, "_vminor");
    append_named_value(output, interp, "_metric_machine");
    append_named_value(output, interp, "_motion_mode");
    append_named_value(output, interp, "_metric");
    append_named_value(output, interp, "_feed");
    append_named_value(output, interp, "_rpm");
    append_named_value(output, interp, "_x");
    append_named_value(output, interp, "_current_tool");
    append_named_value(output, interp, "_ini[traj]max_linear_velocity");
    append_named_value(output, interp, "_hal[standalone.pin-bit]");
    append_named_value(output, interp, "_hal[standalone.signal-float]");
    append_named_value(output, interp, "_hal[standalone.param-s32]");
    append_named_value(output, interp, "_hal[standalone.pin-u32]");
    append_named_value(output, interp, "_hal[standalone.signal-s64]");
    append_named_value(output, interp, "_hal[standalone.param-u64]");
    append_named_value(output, interp, "_hal[standalone.disconnected-float]");
    append_named_value(output, interp, "_hal[standalone.missing]");

    unsetenv("INI_FILE_NAME");
    standalone::reset_hal_adapter();
    return copy_result(output.str());
}

EMSCRIPTEN_KEEPALIVE
void lcinterp_free_string(char *value)
{
    std::free(value);
}

}  // extern "C"
