#include <iostream>
#include <sstream>
#include "machine.h"
#include "string_utils.h"

static bool starts_with(const std::string& line, const std::string& prefix) {
    return line.rfind(prefix, 0) == 0;
}

grbl::machine::machine() {
    pipe = new tcp_transport("192.168.5.39", 23);
}

grbl::machine::~machine() {
    delete pipe;
}

void grbl::machine::connect() {
    pipe->open(*this);
}

void grbl::machine::run_program(const grbl::program& pgm) {
    std::cout << "running program with " << pgm.number_of_instructions() << " instructions" << std::endl;
    running_program = pgm;
    state = grbl_machine_state::run_program;

    executed_instructions = 0;
    continue_program();
}

void grbl::machine::on_connected(grbl::transport *transport) {
    std::cout << "grbl machine connected" << std::endl;
    // telnet handshake so that we get the banner. banner won't be coming otherwise
//        t->send("\xff\xfd\x18\xff\xfd\x20\xff\xfd\x23\xff\xfd\x27");
    pipe->send("\xff\xfd\x18");

}

void grbl::machine::on_disconnected(grbl::transport *transport) {
    std::cout << "grbl machine disconnected" << std::endl;
}


grbl::realtime_status_report grbl::parse_status_report(std::string line, grbl::realtime_status_report& result) {
//    grbl::realtime_status_report result;

    auto l = line.substr(1, -1);
    auto pieces = split_string(l, "|");
    for (auto i = 0; i < pieces.size(); i++) {
        if (i == 0) {
            // status
            auto elements = split_string(pieces[i], ":");
            result.status = status_from_string(elements[0]);
            result.sub_status = elements.size() > 1 ? elements[1] : "";
        } else {
            auto elements = split_string(pieces[i], ":");
            if (elements[0] == "WPos") {
                auto axis = split_string(elements[1], ",");
                result.work_pos[0] = std::stof(axis[0]);
                result.work_pos[1] = std::stof(axis[1]);
                result.work_pos[2] = std::stof(axis[2]);
            } else if (elements[0] == "MPos") {
                auto axis = split_string(elements[1], ",");
                result.machine_pos[0] = std::stof(axis[0]);
                result.machine_pos[1] = std::stof(axis[1]);
                result.machine_pos[2] = std::stof(axis[2]);
            } else if (elements[0] == "Bf") {
                auto p = split_string(elements[1], ",");
                result.buffers_free = std::stoi(p[0]);
                result.rx_chars_free = std::stoi(p[1]);
            } else {
                // not implemented
            }
        }
    }
    return result;
}

grbl::machine_status grbl::status_from_string(const std::string& status) {
    if (status == "Idle") return machine_status::idle;
    if (status == "Run") return machine_status::run;
    if (status == "Hold") return machine_status::hold;
    if (status == "Jog") return machine_status::jog;
    if (status == "Alarm") return machine_status::alarm;
    if (status == "Door") return machine_status::door;
    if (status == "Check") return machine_status::check;
    if (status == "Home") return machine_status::home;
    if (status == "Sleep") return machine_status::sleep;
    if (status == "Tool") return machine_status::tool;
    return machine_status::unknown;
}

std::string grbl::status_to_string(const grbl::machine_status& status) {
    switch (status) {
        case machine_status::idle:
            return "Idle";
        case machine_status::run:
            return "Run";
        case machine_status::hold:
            return "Hold";
        case machine_status::jog:
            return "Jog";
        case machine_status::alarm:
            return "Alarm";
        case machine_status::door:
            return "Door";
        case machine_status::check:
            return "Check";
        case machine_status::home:
            return "Home";
        case machine_status::sleep:
            return "Sleep";
        case machine_status::tool:
            return "Tool";
        case machine_status::unknown:
        default:
            return "Unknown";
    }
}

std::string grbl::alarm_to_string(int alarm) {
    switch (alarm) {
        case 1:
            return "Hard limit has been triggered. Machine position is likely lost due to sudden halt. Re-homing is highly recommended.";
        case 2:
            return "Soft limit alarm. G-code motion target exceeds machine travel. Machine position retained. Alarm may be safely unlocked.";
        case 3:
            return "Reset while in motion. Machine position is likely lost due to sudden halt. Re-homing is highly recommended.";
        case 4:
            return "Probe fail. Probe is not in the expected initial state before starting probe cycle when G38.2 and G38.3 is not triggered and G38.4 and G38.5 is triggered.";
        case 5:
            return "Probe fail. Probe did not contact the workpiece within the programmed travel for G38.2 and G38.4.";
        case 6:
            return "Homing fail. The active homing cycle was reset.";
        case 7:
            return "Homing fail. Safety door was opened during homing cycle.";
        case 8:
            return "Homing fail. Pull off travel failed to clear limit switch. Try increasing pull-off setting or check wiring.";
        case 9:
            return "Homing fail. Could not find limit switch within search distances. Try increasing max travel, decreasing pull-off distance, or check wiring.";
        case 10:
            return "Homing fail. Second dual axis limit switch failed to trigger within configured search distance after first. Try increasing trigger fail distance or check wiring.";
        default:
            return "unknown alarm code";
    }
}

void grbl::machine::on_line_received(std::string line, grbl::transport *transport) {
    if (line.at(0) != '<')
        std::cout << ">> " << line << std::endl;

    if (starts_with(line, "ok")) {
//        on_ok();
        if (state == grbl_machine_state::run_program) {
            continue_program();
        }

    } else if (starts_with(line, "error")) {
        size_t error = std::stoi(line.substr(6));
//        on_error(error);
    } else {
        // we have a push message
        if (starts_with(line, "Grbl")) {
            listener->on_banner(line);
        } else if (starts_with(line, "<")) {
            last_report = parse_status_report(line, last_report);
            listener->on_realtime_status_report(last_report);
        } else if (starts_with(line, "[MSG:")) {
            listener->on_message(line.substr(5, line.size() - 6));
        } else if (starts_with(line, "ALARM:")) {
            listener->on_alarm(std::stoi(line.substr(6)));
        }
    }

//    if (state == grbl_machine_state::run_program) {
//        if (line.rfind("ok", 0) == 0) {
//            continue_program();
//        } else if (line.rfind("error", 0) == 0) {
//            std::cerr << "Received error" << std::endl;
//        } else {
//        }
//    } else {
//        // evaluate responses when not running a program
//    }
}

void grbl::machine::continue_program() {
    if (executed_instructions < running_program.number_of_instructions()) {
        instruction to_send;
        do {
            to_send = running_program.instruction_at(executed_instructions++);
        } while (to_send.type != instruction_type::gcode && executed_instructions < running_program.number_of_instructions());

        if (to_send.type == instruction_type::gcode) {
            pipe->send(to_send.command);
        } else {
            state = grbl_machine_state::idle;
        }
    } else {
        state = grbl_machine_state::idle;
    }
}

void grbl::machine::request_jog(jog_state jog) const {

    cancel_jog();
    if (jog.no_jogging()) {
        return;
    }

    std::stringstream ss;
    ss << "$J=G91 G21 ";

    if (jog.left_pressed)
        ss << " X-1000";
    else if (jog.right_pressed)
        ss << " X1000";

    if (jog.up_pressed)
        ss << " Y1000";
    else if (jog.down_pressed)
        ss << " Y-1000";

    if (jog.z_up_pressed)
        ss << " Z1000";
    else if (jog.z_down_pressed)
        ss << " Z-1000";

    if (jog.speed_fast_pressed) {
        ss << " F10000";
    } else if (jog.speed_slow_pressed) {
        ss << " F100";
    } else {
        ss << " F1000";
    }

    pipe->send(ss.str());
}

void grbl::machine::cancel_jog() const {
    pipe->send_single_char_command(0x85);
}

void grbl::machine::set_listener(grbl::machine_listener *listener) {
    machine::listener = listener;
}

void grbl::machine::request_unlock() {
    pipe->send("$X");
}

void grbl::machine::request_home() {
    pipe->send("$H");
}

void grbl::machine::request_reset() {
    pipe->send_single_char_command(0x18);
}

void grbl::machine::request_cycle_start() {
    pipe->send_single_char_command(0x81);
}

void grbl::machine::request_feed_hold() {
    pipe->send_single_char_command(0x82);
}

bool grbl::jog_state::no_jogging() const {
    return !(up_pressed || down_pressed || left_pressed || right_pressed || z_up_pressed || z_down_pressed);
}
