#include <fstream>
#include <iostream>
#include <iterator>
#include <string>

#include "emc/ini/inifile.hh"

#ifndef LINUXCNC_SOURCE_CONFIG_DIR
#error "LINUXCNC_SOURCE_CONFIG_DIR must point at the LinuxCNC configs/sim tree"
#endif

namespace {

struct UserMCase {
    const char *code;
    const char *remap_code;
    const char *script_rel;
    const char *remap_rel;
    const char *state_mode;
    const char *guard_pin;
    const char *switchkins_target;
    const char *active_g5x;
    const char *work_offset_pocket;
    const char *work_offset_words;
    const char *min_limit_key;
    const char *max_limit_key;
};

std::string source_config_path(const std::string &rel)
{
    return std::string(LINUXCNC_SOURCE_CONFIG_DIR) + "/" + rel;
}

std::string read_text(const std::string &path)
{
    std::ifstream input(path);
    return std::string(
        std::istreambuf_iterator<char>(input),
        std::istreambuf_iterator<char>());
}

bool contains(const std::string &text, const std::string &needle)
{
    return text.find(needle) != std::string::npos;
}

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

bool check_ini_value(
    const linuxcnc::IniFile &ini,
    const std::string &label,
    const std::string &section,
    const std::string &tag,
    const std::string &expected)
{
    const auto actual = ini.findString(tag, section);
    std::cout << label << "_" << section << "." << tag << "="
              << (actual ? *actual : "<missing>") << "\n";
    const bool ok = actual && *actual == expected;
    print_bool(label + "_" + section + "." + tag + "_ok", ok);
    return ok;
}

bool check_axis_targets(
    const linuxcnc::IniFile &ini,
    const UserMCase &user_m,
    const char *axis,
    const char *expected_min,
    const char *expected_max,
    const char *expected_velocity,
    const char *expected_acceleration)
{
    const std::string section = std::string("AXIS_") + axis;
    const std::string label = std::string(user_m.code) + "_" + axis;
    bool ok = true;
    ok &= check_ini_value(ini, label, section, user_m.min_limit_key, expected_min);
    ok &= check_ini_value(ini, label, section, user_m.max_limit_key, expected_max);
    ok &= check_ini_value(ini, label, section, "MAX_VELOCITY", expected_velocity);
    ok &= check_ini_value(ini, label, section, "MAX_ACCELERATION", expected_acceleration);
    return ok;
}

bool check_script(const UserMCase &user_m)
{
    const std::string text = read_text(source_config_path(user_m.script_rel));
    bool ok = true;
    ok &= contains(text, "#!/usr/bin/tclsh");
    ok &= contains(text, "package require Linuxcnc");
    ok &= contains(text, "package require Hal");
    ok &= contains(text, "parse_ini $::env(INI_FILE_NAME)");
    ok &= contains(text, std::string("hal getp ") + user_m.guard_pin);
    ok &= contains(text, "hal setp ini.$l.min_limit");
    ok &= contains(text, "hal setp ini.$l.max_limit");
    ok &= contains(text, "hal setp ini.$l.min_velocity");
    ok &= contains(text, "hal setp ini.$l.max_acceleration");
    ok &= contains(text, user_m.min_limit_key);
    ok &= contains(text, user_m.max_limit_key);
    print_bool(std::string(user_m.code) + "_script_tcl_hal_runtime", ok);
    return ok;
}

bool check_remap_caller(const UserMCase &user_m)
{
    const std::string text = read_text(source_config_path(user_m.remap_rel));
    const std::string remap_code(user_m.remap_code);
    const bool ok =
        contains(text, user_m.code) &&
        contains(text, "#<SWITCHKINS_PIN> = 3") &&
        contains(text, std::string("#<kinstype> = ") + user_m.switchkins_target) &&
        contains(text, "M68") &&
        contains(text, "M68  E#<SWITCHKINS_PIN> Q#<kinstype>") &&
        contains(text, "M66") &&
        contains(text, "M66  E0 L0") &&
        contains(text, std::string("G10 L2 ") + user_m.work_offset_pocket + " " + user_m.work_offset_words) &&
        contains(text, user_m.active_g5x) &&
        contains(text, "#<_hal[motion.switchkins-type]>") &&
        contains(text, std::string("[#<_hal[motion.switchkins-type]> NE ") + user_m.switchkins_target + "]");
    std::cout << remap_code << "_switchkins_output_pin=motion.analog-out-03\n";
    std::cout << remap_code << "_switchkins_target=" << user_m.switchkins_target << "\n";
    std::cout << remap_code << "_work_offset_pocket=" << user_m.work_offset_pocket << "\n";
    std::cout << remap_code << "_work_offset_words=" << user_m.work_offset_words << "\n";
    std::cout << remap_code << "_active_g5x=" << user_m.active_g5x << "\n";
    print_bool(remap_code + "_calls_" + user_m.code, contains(text, user_m.code));
    print_bool(remap_code + "_source_transition_from_linuxcnc", ok);
    print_bool(std::string(user_m.code) + "_remap_calls_user_m_process", ok);
    return ok;
}

bool check_user_m_case(const linuxcnc::IniFile &ini, const UserMCase &user_m)
{
    bool ok = true;
    ok &= check_script(user_m);
    ok &= check_remap_caller(user_m);
    if (std::string(user_m.code) == "M128") {
        ok &= check_axis_targets(ini, user_m, "X", "-300", "300", "60.0", "400.0");
        ok &= check_axis_targets(ini, user_m, "Y", "-100", "100", "60.0", "400.0");
        ok &= check_axis_targets(ini, user_m, "Z", "-240", "0", "60.0", "400.0");
    } else {
        ok &= check_axis_targets(ini, user_m, "X", "-240", "0", "60.0", "400.0");
        ok &= check_axis_targets(ini, user_m, "Y", "-100", "100", "60.0", "400.0");
        ok &= check_axis_targets(ini, user_m, "Z", "-300", "300", "60.0", "400.0");
    }
    print_bool(std::string(user_m.code) + "_source_state_targets_from_linuxcnc", ok);
    return ok;
}

}  // namespace

int main()
{
    const std::string ini_path = source_config_path("axis/vismach/millturn/millturn.ini");
    linuxcnc::IniFile ini(ini_path);
    const bool opened = static_cast<bool>(ini);
    print_bool("millturn_ini_open", opened);
    if (!opened) {
        return 1;
    }

    bool ok = true;
    ok &= check_ini_value(ini, "millturn", "RS274NGC", "USER_M_PATH", "./mcodes");
    ok &= check_ini_value(ini, "millturn", "RS274NGC", "SUBROUTINE_PATH", "./remap_subs");
    ok &= check_ini_value(ini, "millturn", "DISPLAY", "PYVCP", "millturn.xml");
    ok &= check_ini_value(ini, "millturn", "HAL", "HALUI", "halui");
    ok &= check_ini_value(
        ini,
        "millturn",
        "HAL",
        "HALCMD",
        "net :kinstype-select <= motion.analog-out-03 => motion.switchkins-type");

    const UserMCase cases[] = {
        {
            "M128",
            "M428",
            "axis/vismach/millturn/mcodes/M128",
            "axis/vismach/millturn/remap_subs/428remap.ngc",
            "mill",
            "kinstype.is-0",
            "0",
            "G59.1",
            "P7",
            "X-290 Y0 Z-160 A0",
            "MIN_LIMIT",
            "MAX_LIMIT",
        },
        {
            "M129",
            "M429",
            "axis/vismach/millturn/mcodes/M129",
            "axis/vismach/millturn/remap_subs/429remap.ngc",
            "turn",
            "kinstype.is-1",
            "1",
            "G59.2",
            "P8",
            "X-160 Y0 Z-290 A0",
            "MIN_LIMIT_TURN",
            "MAX_LIMIT_TURN",
        },
    };

    for (const auto &user_m : cases) {
        std::cout << user_m.code << "_state_mode=" << user_m.state_mode << "\n";
        std::cout << user_m.code << "_guard_pin=" << user_m.guard_pin << "\n";
        ok &= check_user_m_case(ini, user_m);
    }

    print_bool("millturn_user_m_native_source_state_proof", ok);
    print_bool("millturn_user_m_execution_enabled", false);
    print_bool("millturn_user_m_promotion_allowed", false);
    return ok ? 0 : 1;
}
