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

#include <cmath>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <sstream>
#include <string>

#include "canon_event_sink.hh"
#include "emc/ini/inifile.hh"
#include "emc/rs274ngc/interp_internal.hh"
#include "linuxcnc_hal_adapter.hh"
#include "linuxcnc_tool_adapter.hh"

#ifndef LINUXCNC_VENDOR_DIR
#error "LINUXCNC_VENDOR_DIR must point at wasm-port/vendor/linuxcnc"
#endif

namespace {

std::filesystem::path vendor_path(const std::string &rel)
{
    return std::filesystem::path(LINUXCNC_VENDOR_DIR) / rel;
}

void print_bool(const std::string &name, bool value)
{
    std::cout << name << "=" << (value ? 1 : 0) << "\n";
}

void print_error_text(Interp &interp, const std::string &name, int rc)
{
    char error_buf[LINELEN] = {0};
    interp.error_text(rc, error_buf, sizeof(error_buf));
    std::cout << name << error_buf << "\n";
}

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

    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 = 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;
    std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
    interp.init_named_parameters();
}

std::filesystem::path subroutine_path(const std::filesystem::path &machine_dir,
                                      const std::string &entry)
{
    const std::filesystem::path path(entry);
    if (path.is_absolute()) {
        return path;
    }
    return machine_dir / path;
}

bool configure_remaps(Interp &interp,
                      const std::filesystem::path &ini_path,
                      const char *label,
                      int expected_remaps)
{
    linuxcnc::IniFile ini(ini_path.string());
    const bool opened = static_cast<bool>(ini);
    print_bool(std::string(label) + "_ini_open", opened);
    if (!opened) {
        return false;
    }

    const auto machine_dir = ini_path.parent_path();
    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::stringstream paths(*subroutine_path_text);
        std::string entry;
        while (std::getline(paths, entry, ':') && subroutine_count < MAX_SUB_DIRS) {
            const auto resolved = subroutine_path(machine_dir, entry);
            interp._setup.subroutines[subroutine_count++] = strstore(resolved.c_str());
        }
    }
    std::cout << label << "_subroutine_path_count=" << subroutine_count << "\n";

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

    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);
        std::cout << label << "_parse_remap_" << remap_count << "=" << rc << "\n";
        ok &= rc == INTERP_OK;
    }
    remap_count -= 1;
    std::cout << label << "_parsed_remap_count=" << remap_count << "\n";
    ok &= remap_count == expected_remaps;

    print_bool(std::string(label) + "_remaps_ready", ok);
    return ok;
}

bool execute_file(Interp &interp,
                  const std::filesystem::path &program_path,
                  const char *label,
                  bool continue_on_error,
                  bool expect_error)
{
    bool ok = true;
    const int open_rc = interp.open(program_path.c_str());
    std::cout << label << "_file_open=" << open_rc << "\n";
    if (open_rc != INTERP_OK) {
        return false;
    }

    constexpr int max_file_steps = 20000;
    int read_count = 0;
    int execute_count = 0;
    int finish_count = 0;
    int final_rc = INTERP_OK;
    bool reached_exit = false;
    bool reached_endfile = false;
    bool saw_error = false;

    while (read_count < max_file_steps && execute_count < max_file_steps) {
        const int read_rc = interp.read();
        if (read_rc == INTERP_ENDFILE) {
            final_rc = read_rc;
            reached_endfile = true;
            break;
        }

        ++read_count;
        if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
            std::cout << label << "_file_read_" << read_count << "=" << read_rc << "\n";
            print_error_text(interp, std::string(label) + "_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();
        ++execute_count;
        final_rc = execute_rc;
        if (execute_rc == INTERP_EXECUTE_FINISH) {
            ++finish_count;
        }
        if ((execute_rc != INTERP_OK) &&
            (execute_rc != INTERP_EXECUTE_FINISH) &&
            (execute_rc != INTERP_EXIT)) {
            std::cout << label << "_file_execute_" << execute_count << "=" << execute_rc << "\n";
            print_error_text(interp, std::string(label) + "_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;
        }
    }

    std::cout << label << "_file_read_count=" << read_count << "\n";
    std::cout << label << "_file_execute_count=" << execute_count << "\n";
    std::cout << label << "_file_finish_count=" << finish_count << "\n";
    std::cout << label << "_file_final_rc=" << final_rc << "\n";
    print_bool(std::string(label) + "_file_reached_exit", reached_exit);
    print_bool(std::string(label) + "_file_reached_endfile", reached_endfile);
    print_bool(std::string(label) + "_file_saw_error", saw_error);
    std::cout << label << "_canon_event_count=" << standalone::canon_events().size() << "\n";
    for (const auto &event : standalone::canon_events()) {
        std::cout << label << "_canon_event=" << event << "\n";
    }

    ok &= read_count > 0;
    ok &= execute_count > 0;
    ok &= read_count < max_file_steps;
    ok &= execute_count < max_file_steps;
    ok &= reached_exit || reached_endfile;
    ok &= expect_error ? saw_error : !saw_error;
    print_bool(std::string(label) + "_linuxcnc_remap_file_execute", ok);
    return ok;
}

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;
}

bool execute_mdi(Interp &interp, const char *label, const char *command)
{
    int rc = interp.execute(command);
    std::cout << label << "_mdi_" << command_label(command) << "_execute_0=" << rc << "\n";

    int finish_count = 0;
    while (rc == INTERP_EXECUTE_FINISH && finish_count < 16) {
        ++finish_count;
        rc = interp.execute();
        std::cout << label << "_mdi_" << command_label(command)
                  << "_execute_" << finish_count << "=" << rc << "\n";
    }

    std::cout << label << "_mdi_" << command_label(command)
              << "_execute_finish_count=" << finish_count << "\n";
    return rc == INTERP_OK;
}

bool execute_remap_io_mdi_case()
{
    const auto test_dir = vendor_path("tests/remap/remap-io");
    const auto ini_path = test_dir / "test-ngc.ini";
    setenv("INI_FILE_NAME", ini_path.c_str(), 1);

    Interp interp;
    initialize_interp(interp);

    bool ok = configure_remaps(interp, ini_path, "remap_io_ngc", 7);

    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_mdi(interp, "remap_io_ngc", step.command);
        std::cout << "remap_io_ngc_parameter_5399=" << interp._setup.parameters[5399] << "\n";
        std::cout << "remap_io_ngc_parameter_100=" << interp._setup.parameters[100] << "\n";
    }

    std::cout << "remap_io_ngc_canon_event_count=" << standalone::canon_events().size() << "\n";
    for (const auto &event : standalone::canon_events()) {
        std::cout << "remap_io_ngc_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;
    print_bool("remap_io_ngc_linuxcnc_mdi_sequence", ok);
    unsetenv("INI_FILE_NAME");
    return ok;
}

}  // namespace

int main()
{
    struct RegressionCase {
        const char *label;
        const char *rel;
        int expected_remaps;
        bool continue_on_error;
        bool expect_error;
    };

    const RegressionCase cases[] = {
        {"duplicate_oword", "tests/remap/duplicate-o-word", 2, false, false},
        {"fail_args_0", "tests/remap/fail/args.0", 1, false, false},
        {"fail_args_1", "tests/remap/fail/args.1", 1, true, true},
        {"fail_args_2", "tests/remap/fail/args.2", 1, true, true},
        {"fail_body_ngc", "tests/remap/fail/body-ngc", 1, true, true},
        {"m30_interaction", "tests/remap/m30-interaction", 1, false, false},
        {"nested_remaps_oword", "tests/remap/nested-remaps-oword", 4, false, false},
        {"posargs_0", "tests/remap/posargs.0", 1, false, false},
        {"sequencing", "tests/remap/sequencing", 7, false, false},
    };

    bool all_ok = true;
    for (const auto &test : cases) {
        const auto test_dir = vendor_path(test.rel);
        const auto ini_path = test_dir / "test.ini";
        const auto program_path = test_dir / "test.ngc";

        setenv("INI_FILE_NAME", ini_path.c_str(), 1);

        Interp interp;
        initialize_interp(interp);

        bool ok = configure_remaps(interp, ini_path, test.label, test.expected_remaps);
        ok &= execute_file(interp,
                           program_path,
                           test.label,
                           test.continue_on_error,
                           test.expect_error);
        print_bool(std::string(test.label) + "_linuxcnc_regression_path", ok);
        all_ok &= ok;

        unsetenv("INI_FILE_NAME");
    }

    all_ok &= execute_remap_io_mdi_case();

    print_bool("linuxcnc_upstream_remap_regression_path", all_ok);
    return all_ok ? 0 : 1;
}
