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

#include <cctype>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <sstream>
#include <string>
#include <sys/stat.h>

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

namespace standalone {

namespace {

constexpr int kDefaultAxisMask = Interp::AXIS_MASK_X | Interp::AXIS_MASK_Y | Interp::AXIS_MASK_Z;
constexpr int kMaxUserMDirs = 6;

std::string g_user_m_paths[USER_DEFINED_FUNCTION_NUM];

bool is_executable_file(const std::filesystem::path &path)
{
    struct stat st {};
    return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode) && (st.st_mode & S_IXUSR);
}

std::filesystem::path resolve_machine_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();
}

void standalone_user_m_code(int num, double arg1, double arg2)
{
    FINISH();
    const int code = num + 100;
    std::ostringstream event;
    event << "USER_M_COMMAND code=M" << code
          << " path=" << g_user_m_paths[num]
          << " p=" << arg1
          << " q=" << arg2;
    push_canon_event(event.str());
}

int axis_mask_for_letter(char letter)
{
    switch (std::toupper(static_cast<unsigned char>(letter))) {
    case 'X':
        return Interp::AXIS_MASK_X;
    case 'Y':
        return Interp::AXIS_MASK_Y;
    case 'Z':
        return Interp::AXIS_MASK_Z;
    case 'A':
        return Interp::AXIS_MASK_A;
    case 'B':
        return Interp::AXIS_MASK_B;
    case 'C':
        return Interp::AXIS_MASK_C;
    case 'U':
        return Interp::AXIS_MASK_U;
    case 'V':
        return Interp::AXIS_MASK_V;
    case 'W':
        return Interp::AXIS_MASK_W;
    default:
        return 0;
    }
}

}  // namespace

int axis_mask_from_coordinates(const char *coordinates)
{
    if (!coordinates || coordinates[0] == '\0') {
        return kDefaultAxisMask;
    }

    int axis_mask = 0;
    for (const char *cursor = coordinates; *cursor; ++cursor) {
        axis_mask |= axis_mask_for_letter(*cursor);
    }
    return axis_mask == 0 ? kDefaultAxisMask : axis_mask;
}

int axis_mask_from_ini(const char *ini_path)
{
    if (!ini_path || ini_path[0] == '\0') {
        return kDefaultAxisMask;
    }

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

    if (auto coordinates = ini.findString("COORDINATES", "TRAJ")) {
        return axis_mask_from_coordinates(coordinates->c_str());
    }
    return kDefaultAxisMask;
}

void apply_axis_mask_to_interp(Interp &interp, int axis_mask)
{
    if (axis_mask == 0) {
        axis_mask = kDefaultAxisMask;
    }

    set_external_axis_mask(axis_mask);
    std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
    if (!(axis_mask & Interp::AXIS_MASK_X)) {
        interp._readers[static_cast<int>('x')] = nullptr;
    }
    if (!(axis_mask & Interp::AXIS_MASK_Y)) {
        interp._readers[static_cast<int>('y')] = nullptr;
    }
    if (!(axis_mask & Interp::AXIS_MASK_Z)) {
        interp._readers[static_cast<int>('z')] = nullptr;
    }
    if (!(axis_mask & Interp::AXIS_MASK_A)) {
        interp._readers[static_cast<int>('a')] = nullptr;
    }
    if (!(axis_mask & Interp::AXIS_MASK_B)) {
        interp._readers[static_cast<int>('b')] = nullptr;
    }
    if (!(axis_mask & Interp::AXIS_MASK_C)) {
        interp._readers[static_cast<int>('c')] = nullptr;
    }
    if (!(axis_mask & Interp::AXIS_MASK_U)) {
        interp._readers[static_cast<int>('u')] = nullptr;
    }
    if (!(axis_mask & Interp::AXIS_MASK_V)) {
        interp._readers[static_cast<int>('v')] = nullptr;
    }
    if (!(axis_mask & Interp::AXIS_MASK_W)) {
        interp._readers[static_cast<int>('w')] = nullptr;
    }
}

void apply_machine_axis_config(Interp &interp, const linuxcnc::IniFile &ini)
{
    interp._setup.orient_offset = ini.findRealV("ORIENT_OFFSET", "RS274NGC", 0.0);
    interp._setup.disable_fanuc_style_sub =
        ini.findBoolV("DISABLE_FANUC_STYLE_SUB", "RS274NGC", false);

    if (auto coordinates = ini.findString("COORDINATES", "TRAJ")) {
        apply_axis_mask_to_interp(interp, axis_mask_from_coordinates(coordinates->c_str()));
        return;
    }
    apply_axis_mask_to_interp(interp, kDefaultAxisMask);
}

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

    linuxcnc::IniFile ini(ini_path);
    if (!ini) {
        apply_axis_mask_to_interp(interp, kDefaultAxisMask);
        return false;
    }
    apply_machine_axis_config(interp, ini);
    return true;
}

int load_tool_table_from_ini(Interp &interp, const linuxcnc::IniFile &ini, const char *machine_dir)
{
    if (auto tool_table = ini.findString("TOOL_TABLE", "EMCIO")) {
        const std::filesystem::path base_dir = (machine_dir && machine_dir[0] != '\0')
            ? std::filesystem::path(machine_dir)
            : std::filesystem::path();
        const std::filesystem::path tool_path = resolve_machine_path(base_dir, *tool_table);
        const bool random_toolchanger = ini.findBoolV("RANDOM_TOOLCHANGER", "EMCIO", false);

        interp._setup.random_toolchanger = random_toolchanger;
        tooldata_init(random_toolchanger);
        tooldata_set_db(DB_NOTUSED);
        const int rc = tooldata_load(tool_path.c_str());
        if (rc != 0) {
            return rc;
        }
        return interp.load_tool_table();
    }

    return INTERP_OK;
}

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

    linuxcnc::IniFile ini(ini_path);
    if (!ini) {
        return -1;
    }
    const std::filesystem::path machine_dir = std::filesystem::path(ini_path).parent_path();
    return load_tool_table_from_ini(interp, ini, machine_dir.c_str());
}

void reset_user_m_code_registry()
{
    for (int index = 0; index < USER_DEFINED_FUNCTION_NUM; ++index) {
        USER_DEFINED_FUNCTION[index] = nullptr;
        g_user_m_paths[index].clear();
    }
}

int register_user_m_codes_from_ini(const linuxcnc::IniFile &ini, const char *machine_dir)
{
    std::filesystem::path base_dir = (machine_dir && machine_dir[0] != '\0')
        ? std::filesystem::path(machine_dir)
        : std::filesystem::current_path();

    std::string program_prefix = "nc_files";
    if (auto prefix = ini.findString("PROGRAM_PREFIX", "DISPLAY")) {
        program_prefix = *prefix;
    }

    std::filesystem::path dirs[kMaxUserMDirs];
    int dir_count = 0;
    dirs[dir_count++] = resolve_machine_path(base_dir, program_prefix);

    if (auto user_m_path = ini.findString("USER_M_PATH", "RS274NGC")) {
        std::istringstream entries(*user_m_path);
        std::string entry;
        while (std::getline(entries, entry, ':') && dir_count < kMaxUserMDirs) {
            if (!entry.empty()) {
                dirs[dir_count++] = resolve_machine_path(base_dir, entry);
            }
        }
    }

    int registered = 0;
    for (int num = 0; num < USER_DEFINED_FUNCTION_NUM; ++num) {
        for (int dir_index = 0; dir_index < dir_count; ++dir_index) {
            char file_name[8] = {0};
            std::snprintf(file_name, sizeof(file_name), "M1%02d", num);
            std::filesystem::path candidate = dirs[dir_index] / file_name;
            if (!is_executable_file(candidate)) {
                continue;
            }
            USER_DEFINED_FUNCTION_ADD(standalone_user_m_code, num);
            g_user_m_paths[num] = candidate.string();
            ++registered;
            break;
        }
    }
    return registered;
}

int register_user_m_codes_from_ini(const char *ini_path)
{
    if (!ini_path || ini_path[0] == '\0') {
        return 0;
    }

    linuxcnc::IniFile ini(ini_path);
    if (!ini) {
        return 0;
    }
    const std::filesystem::path machine_dir = std::filesystem::path(ini_path).parent_path();
    return register_user_m_codes_from_ini(ini, machine_dir.c_str());
}

}  // namespace standalone
