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

#include <cstring>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#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"

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_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 apply_ini_search_paths(Interp &interp, const char *ini_path)
{
    if (!ini_path || ini_path[0] == '\0') {
        return;
    }

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

    const auto machine_dir = std::filesystem::path(ini_path).parent_path();
    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::stringstream paths(*subroutine_path_text);
        std::string entry;
        while (std::getline(paths, entry, ':') && subroutine_count < MAX_SUB_DIRS) {
            std::filesystem::path resolved(entry);
            if (resolved.is_relative()) {
                resolved = machine_dir / resolved;
            }
            interp._setup.subroutines[subroutine_count++] = strstore(resolved.c_str());
        }
    }

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

std::filesystem::path resolve_machine_relative_path(const std::filesystem::path &machine_dir,
                                                     const std::string &entry)
{
    std::filesystem::path path(entry);
    if (path.is_relative()) {
        path = machine_dir / path;
    }
    return path.lexically_normal();
}

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 auto resolved = resolve_machine_relative_path(
        std::filesystem::path(ini_path).parent_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();
}

std::vector<std::string> load_program(int argc, char **argv)
{
    if (argc <= 1) {
        return {
            "G0 X1.0 Y2.0 (Comment)\n",
            "G1 X3.0 Y4.0 F120.0\n",
        };
    }

    std::ifstream input(argv[1]);
    if (!input) {
        std::cerr << "failed to open fixture: " << argv[1] << "\n";
        std::exit(2);
    }

    std::vector<std::string> lines;
    std::string line;
    while (std::getline(input, line)) {
        if (!line.empty()) {
            lines.push_back(line + "\n");
        }
    }
    return lines;
}

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::ostream &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::ostream &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";
}

}  // namespace

int main(int argc, char **argv)
{
    bool continue_on_error = false;
    int arg_offset = 1;
    if (argc > 1 && std::strcmp(argv[1], "--continue-on-error") == 0) {
        continue_on_error = true;
        arg_offset = 2;
    }

    Interp interp;
    standalone::reset_canon_events();
    const char *ini_path = argc > arg_offset + 1 ? argv[arg_offset + 1] : nullptr;
    if (ini_path && ini_path[0] != '\0') {
        setenv("INI_FILE_NAME", ini_path, 1);
    } else {
        unsetenv("INI_FILE_NAME");
    }
    initialize_minimal_interp(interp);
    apply_ini_search_paths(interp, ini_path);

    const std::vector<std::string> program = load_program(argc - arg_offset + 1, argv + arg_offset - 1);
    if (program.empty()) {
        std::cerr << "empty fixture\n";
        return 2;
    }

    char line[LINELEN] = {0};
    std::strncpy(line, program.front().c_str(), sizeof(line) - 1);
    char raw_line[LINELEN] = {0};
    char cooked_line[LINELEN] = {0};
    int length = -1;

    const int read_text_rc = interp.read_text(line, nullptr, raw_line, cooked_line, &length);
    const int downcase_rc = interp.close_and_downcase(line);
    const int read_rc = interp.read(program.front().c_str());

    initialize_minimal_interp(interp);
    apply_ini_search_paths(interp, ini_path);
    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());
        std::cout << "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));
            std::cout << "error_text=" << error_buf << "\n";
        }
    }

    int file_open_rc = INTERP_FILE_NOT_OPEN;
    int file_read_count = 0;
    int file_execute_count = 0;
    bool file_saw_error = false;
    if (argc > arg_offset) {
        Interp file_interp;
        standalone::reset_canon_events();
        initialize_minimal_interp(file_interp);
        apply_ini_search_paths(file_interp, ini_path);
        const int init_rc = initialize_parameter_file_from_ini(file_interp, ini_path);
        if (init_rc != INTERP_OK) {
            std::cout << "file_init=" << init_rc << "\n";
            if (init_rc > INTERP_MIN_ERROR) {
                char error_buf[LINELEN] = {0};
                file_interp.error_text(init_rc, error_buf, sizeof(error_buf));
                std::cout << "file_error_text=" << error_buf << "\n";
            }
        }
        apply_ini_search_paths(file_interp, ini_path);
        file_open_rc = init_rc == INTERP_OK ? file_interp.open(argv[arg_offset]) : init_rc;
        if (file_open_rc == INTERP_OK) {
            while (true) {
                const int file_read_rc = file_interp.read();
                if (file_read_rc == INTERP_ENDFILE) {
                    std::cout << "file_read_eof=" << file_read_rc << "\n";
                    break;
                }
                ++file_read_count;
                std::cout << "file_read_" << file_read_count << "=" << file_read_rc << "\n";
                append_run_step(std::cout, file_interp, "read", file_read_count, file_read_rc);
                if ((file_read_rc != INTERP_OK) && (file_read_rc != INTERP_EXECUTE_FINISH)) {
                    if (file_read_rc > INTERP_MIN_ERROR) {
                        char error_buf[LINELEN] = {0};
                        file_interp.error_text(file_read_rc, error_buf, sizeof(error_buf));
                        std::cout << "file_error_text=" << error_buf << "\n";
                    }
                    file_saw_error = true;
                    if (!continue_on_error) {
                        break;
                    }
                    continue;
                }

                const int file_execute_rc = file_interp.execute();
                ++file_execute_count;
                std::cout << "file_execute_" << file_execute_count << "=" << file_execute_rc << "\n";
                append_run_step(std::cout, file_interp, "execute", file_execute_count, file_execute_rc);
                if (file_execute_rc > INTERP_MIN_ERROR) {
                    char error_buf[LINELEN] = {0};
                    file_interp.error_text(file_execute_rc, error_buf, sizeof(error_buf));
                    std::cout << "file_error_text=" << error_buf << "\n";
                }
                if ((file_execute_rc != INTERP_OK) &&
                    (file_execute_rc != INTERP_EXECUTE_FINISH) &&
                    (file_execute_rc != INTERP_EXIT)) {
                    file_saw_error = true;
                    break;
                }
                if (file_execute_rc == INTERP_EXIT) {
                    break;
                }
            }
        }
    }

    block block{};
    const int init_block_rc = interp.init_block(&block);
    int parse_line_rc = INTERP_ERROR;
    if (read_text_rc == INTERP_OK) {
        parse_line_rc = interp.parse_line(cooked_line, &block, &interp._setup);
    }

    std::cout << "read_text=" << read_text_rc << "\n";
    std::cout << "file_open=" << file_open_rc << "\n";
    std::cout << "file_read_count=" << file_read_count << "\n";
    std::cout << "file_execute_count=" << file_execute_count << "\n";
    std::cout << "file_saw_error=" << (file_saw_error ? 1 : 0) << "\n";
    std::cout << "raw_line=" << raw_line << "\n";
    std::cout << "cooked_line=" << cooked_line << "\n";
    std::cout << "line_length=" << length << "\n";
    std::cout << "read=" << read_rc << "\n";
    std::cout << "setup.linetext=" << interp._setup.linetext << "\n";
    std::cout << "setup.blocktext=" << interp._setup.blocktext << "\n";
    std::cout << "setup.line_length=" << interp._setup.line_length << "\n";
    std::cout << "setup.sequence_number=" << interp._setup.sequence_number << "\n";
    std::cout << "setup.current_x=" << interp._setup.current_x << "\n";
    std::cout << "setup.current_y=" << interp._setup.current_y << "\n";
    std::cout << "setup.current_z=" << interp._setup.current_z << "\n";
    std::cout << "setup.parameter_5420=" << interp._setup.parameters[5420] << "\n";
    std::cout << "setup.parameter_5421=" << interp._setup.parameters[5421] << "\n";
    std::cout << "setup.parameter_5422=" << interp._setup.parameters[5422] << "\n";
    std::cout << "setup.feed_rate=" << interp._setup.feed_rate << "\n";
    std::cout << "post_execute.plane=" << static_cast<int>(interp._setup.plane) << "\n";
    std::cout << "post_execute.distance_mode=" << static_cast<int>(interp._setup.distance_mode) << "\n";
    std::cout << "post_execute.feed_mode=" << static_cast<int>(interp._setup.feed_mode) << "\n";
    std::cout << "post_execute.motion_mode=" << interp._setup.motion_mode << "\n";
    std::cout << "post_execute.origin_index=" << interp._setup.origin_index << "\n";
    std::cout << "post_execute.feed_override=" << interp._setup.feed_override << "\n";
    std::cout << "post_execute.speed_override_0=" << interp._setup.speed_override[0] << "\n";
    std::cout << "post_execute.spindle_turning_0=" << interp._setup.spindle_turning[0] << "\n";
    std::cout << "post_execute.mist=" << interp._setup.mist << "\n";
    std::cout << "post_execute.flood=" << interp._setup.flood << "\n";
    std::cout << "close_and_downcase=" << downcase_rc << "\n";
    std::cout << "normalized_line=" << line << "\n";
    std::cout << "init_block=" << init_block_rc << "\n";
    std::cout << "parse_line=" << parse_line_rc << "\n";
    std::cout << "block.motion_to_be=" << block.motion_to_be << "\n";
    std::cout << "block.g_modes[GM_MOTION]=" << block.g_modes[GM_MOTION] << "\n";
    std::cout << "block.x_flag=" << block.x_flag << " x=" << block.x_number << "\n";
    std::cout << "block.y_flag=" << block.y_flag << " y=" << block.y_number << "\n";
    std::cout << "executing_block.motion_to_be=" << EXECUTING_BLOCK(interp._setup).motion_to_be << "\n";
    std::cout << "executing_block.g_modes[GM_MOTION]=" << EXECUTING_BLOCK(interp._setup).g_modes[GM_MOTION] << "\n";
    std::cout << "executing_block.x_flag=" << EXECUTING_BLOCK(interp._setup).x_flag
              << " x=" << EXECUTING_BLOCK(interp._setup).x_number << "\n";
    std::cout << "executing_block.y_flag=" << EXECUTING_BLOCK(interp._setup).y_flag
              << " y=" << EXECUTING_BLOCK(interp._setup).y_number << "\n";
    std::cout << "executing_block.f_flag=" << EXECUTING_BLOCK(interp._setup).f_flag
              << " f=" << EXECUTING_BLOCK(interp._setup).f_number << "\n";
    std::cout << "call_level=" << interp.call_level() << "\n";
    std::cout << "loggingLevel=" << interp._setup.loggingLevel << "\n";
    std::cout << "canon_event_count=" << standalone::canon_events().size() << "\n";
    for (const auto &event : standalone::canon_events()) {
        std::cout << "canon_event=" << event << "\n";
    }

    return 0;
}
