#include "sender_app.h"

SenderApp::SenderApp()
        : nanogui::Screen(nanogui::Vector2i(1024, 768), "GRBL Sender") {
    inc_ref();

    window = new nanogui::Window(this, "Machine status");
    window->set_position(nanogui::Vector2i(0, 0));
    window->set_layout(new nanogui::BoxLayout(nanogui::Orientation::Vertical));

    tab_widget = window->add<nanogui::TabWidget>();
    tab_widget->set_tabs_draggable(true);
    tab_widget->set_callback([&](int index) {
        tab_widget->set_selected_index(index);
    });

    info_vscroll = tab_widget->add<nanogui::VScrollPanel>();
    tab_widget->append_tab("Info", info_vscroll);

    info_layer = info_vscroll->add<Widget>();
    info_layer->set_layout(new nanogui::GroupLayout(10, 20, 30, 0));
    info_layer->add<nanogui::Label>("");

    auto probe_layer = tab_widget->add<Widget>();
    probe_layer->set_layout(new nanogui::GroupLayout(10, 20, 30, 0));
    probe_layer->add<nanogui::Label>("");

    add_status_markup();
    add_dro_markup();
    add_program_extents();
    add_pins_markup();
    add_jogging_markup();
    add_work_parameters_markup();
    add_z_probe_markup();

    heightmap_layer = tab_widget->add<Widget>();
    heightmap_layer->set_layout(new nanogui::GroupLayout(10, 20, 30, 0));
    heightmap_layer->add<nanogui::Label>("");
    tab_widget->append_tab("Heightmap", heightmap_layer);
    add_heightmap_markup();

    perform_layout();

    m_render_pass = new nanogui::RenderPass({this}, nullptr, nullptr, nullptr, true);
    m_render_pass->set_clear_color(0, nanogui::Color(0.13f, 0.13f, 0.13f, 1.f));
    m_render_pass->set_depth_test(nanogui::RenderPass::DepthTest::Greater, true);
    m_render_pass->set_cull_mode(nanogui::RenderPass::CullMode::Disabled);

    cnc.connect();
}

void SenderApp::add_z_probe_markup() {
    info_layer->add<nanogui::Label>("Z-Probe", "sans-bold", 20);
    auto z_probe_holder = info_layer->add<nanogui::Widget>();
    z_probe_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 2, 2));

    auto another_holder = z_probe_holder->add<Widget>();
    another_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 2, nanogui::Alignment::Fill, 2, 2));

    another_holder->add<nanogui::Label>("Feed", "sans-bold");
    txt_feed = another_holder->add<nanogui::TextBox>("100");
    txt_feed->set_editable(true);
    txt_feed->set_fixed_width(100);

    another_holder->add<nanogui::Label>("Min Z", "sans-bold");
    txt_min_z = another_holder->add<nanogui::TextBox>("-65");
    txt_min_z->set_editable(true);
    txt_min_z->set_fixed_width(100);

    z_probe_holder->add<nanogui::Label>("");
    auto btn_start_probing = z_probe_holder->add<nanogui::Button>("Start probing");
    btn_start_probing->set_callback([&]() {
        cnc.start_z_probe(std::stof(txt_min_z->value()), std::stof(txt_feed->value()));
    });

}

void SenderApp::add_work_parameters_markup() {
    // work parameters
    info_layer->add<nanogui::Label>("Work parameters", "sans-bold", 20);
    auto work_holder = info_layer->add<nanogui::Widget>();
    work_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Fill, 6, 6));

    auto work_data_holder = work_holder->add<nanogui::Widget>();
    work_data_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 2, nanogui::Alignment::Fill, 6, 6));

    work_data_holder->add<nanogui::Label>("Offset", "sans-bold");
    std::vector<std::string> offset_items = {"G54", "G55", "G56", "G57", "G58", "G59"};
    cbo_work_offset = work_data_holder->add<nanogui::ComboBox>(offset_items);
    cbo_work_offset->set_callback([&](int idx) {
        refresh_offset();
        update_dro();
    });

    work_data_holder->add<nanogui::Label>("Tool", "sans-bold");
    std::vector<std::string> tool_items = {"None"};
    cbo_tool = work_data_holder->add<nanogui::ComboBox>(tool_items);

    auto work_btn_holder = work_holder->add<nanogui::Widget>();
    work_btn_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 4, nanogui::Alignment::Fill, 0, 6));


    btn_load_program = work_btn_holder->add<nanogui::Button>("Load");
    btn_load_program->set_callback([&] {
        auto path = nanogui::file_dialog(
                {{"gcode", "G-Code files"},
                 {"nc",    "G-Code files"},
                 {"ngc",   "G-Code files"}}, true);

        if (pgm.load_from_file(path)) {
            init_program_geometry();
            set_program_extents();
            fill_heightmap_from_model();
            update_grid();
        } else {
        }
    });
    btn_load_program->set_tooltip("Load program");

    btn_check_program = work_btn_holder->add<nanogui::Button>("Check");
//        btnCheckProgram->set_enabled(false);
    btn_check_program->set_callback([&] {
        cnc.check_program(pgm);
    });
    btn_check_program->set_tooltip("Check program");

    btn_run_program = work_btn_holder->add<nanogui::Button>("Run");
//        btnRunProgram->set_enabled(false);
    btn_run_program->set_callback([&] {
        auto leveled_pgm = pgm.apply_heightmap(heightmap_grid);
        leveled_pgm.save("last-run-autoleveled.nc");
        cnc.run_program(btn_run_autoleveled->pushed() ? leveled_pgm : pgm,
                        "G" + std::to_string(cbo_work_offset->selected_index() + 54));
    });
    btn_run_program->set_tooltip("Execute program");

    btn_run_autoleveled = work_btn_holder->add<nanogui::Button>("", FA_TABLE);
    btn_run_autoleveled->set_flags(nanogui::Button::ToggleButton);
    btn_run_autoleveled->set_tooltip("Run auto-leveled program version, using the current heightmap");
}

void SenderApp::add_status_markup() {
    auto status_holder = info_layer->add<nanogui::Widget>();
    status_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 0));

    auto status_text_holder = status_holder->add<nanogui::Widget>();
    status_text_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 3, nanogui::Alignment::Fill, 0, 0));

    status_text_holder->add<nanogui::Label>("Status", "sans-bold", 20);

    txt_status = status_text_holder->add<nanogui::TextBox>();
    txt_status->set_editable(false);
    txt_status->set_fixed_width(300);

    auto *btn_reset = status_text_holder->add<nanogui::Button>("Reset");
    btn_reset->set_background_color(color_red);
    btn_reset->set_callback([&] {
        cnc.request_reset();
    });

    //
    status_text_holder->add<nanogui::Label>("");

    txt_message = status_text_holder->add<nanogui::TextBox>("");
    txt_message->set_fixed_width(300);

    status_text_holder->add<nanogui::Label>("");

    // buttons
    auto status_btn_holder = status_holder->add<nanogui::Widget>();
    status_btn_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 2, nanogui::Alignment::Fill, 6, 6));

    auto *btn_homing = new nanogui::Button(status_btn_holder, "Homing", FA_HOME);
    btn_homing->set_callback([&] {
        cnc.request_home();
    });

    auto *btn_unlock = new nanogui::Button(status_btn_holder, "Unlock", FA_UNLOCK);
    btn_unlock->set_callback([&] {
        cnc.request_unlock();
    });

    // buttons to change state
    auto *btn_cycle_start = status_btn_holder->add<nanogui::Button>("Cycle Start", FA_PLAY);
    btn_cycle_start->set_callback([&] {
        cnc.request_cycle_start();
    });
    auto *btn_feed_hold = status_btn_holder->add<nanogui::Button>("Feed Hold", FA_PAUSE);
    btn_feed_hold->set_callback([&] {
        cnc.request_feed_hold();
    });

}

void SenderApp::add_jogging_markup() {
    // jogging
    info_layer->add<nanogui::Label>("Jogging", "sans-bold", 20);
    auto holder = new nanogui::Widget(info_layer);

    holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 3, nanogui::Alignment::Fill, 0, 0));

    // feed and distance
    auto jog_params_holder = holder->add<nanogui::Widget>();
    jog_params_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 3, nanogui::Alignment::Fill, 0, 4));

    jog_params_holder->add<nanogui::Label>("Feed", "sans-bold");
    cbo_jog_feed_rates = jog_params_holder->add<nanogui::ComboBox>(jog_feed_rates);
    cbo_jog_feed_rates->set_selected_index(2);
    jog_params_holder->add<nanogui::Label>("mm/min");

    jog_params_holder->add<nanogui::Label>("Distance", "sans-bold");
    cbo_jog_distance = jog_params_holder->add<nanogui::ComboBox>(jog_distances);
    cbo_jog_distance->set_selected_index(2);
    jog_params_holder->add<nanogui::Label>("mm");

    jog_params_holder->add<nanogui::Label>("Keyboard Jog", "sans-bold");
    btn_keyboard_jog = jog_params_holder->add<nanogui::Button>("", FA_KEYBOARD);
    btn_keyboard_jog->set_flags(nanogui::Button::ToggleButton);
    jog_params_holder->add<nanogui::Label>("");

    // X and Y
    auto jog_btn_holder = holder->add<nanogui::Widget>();
    jog_btn_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 3, nanogui::Alignment::Fill, 0, 6));

    jog_btn_holder->add<nanogui::Label>("");
    auto jog_btn = jog_btn_holder->add<nanogui::Button>("", FA_ARROW_ALT_CIRCLE_UP);
    jog_btn->set_callback([&]() {
        // FIXME: get rid of std::stof
        cnc.request_jog_fixed(grbl::jog_direction::y_up,
                              std::stof(jog_distances[cbo_jog_distance->selected_index()]),
                              std::stof(jog_feed_rates[cbo_jog_feed_rates->selected_index()]));
    });
    jog_btn_holder->add<nanogui::Label>("");

    jog_btn = jog_btn_holder->add<nanogui::Button>("", FA_ARROW_ALT_CIRCLE_LEFT);
    jog_btn->set_callback([&]() {
        // FIXME: get rid of std::stof
        cnc.request_jog_fixed(grbl::jog_direction::x_down,
                              std::stof(jog_distances[cbo_jog_distance->selected_index()]),
                              std::stof(jog_feed_rates[cbo_jog_feed_rates->selected_index()]));
    });

    jog_btn = jog_btn_holder->add<nanogui::Button>("", FA_HOME);
    jog_btn->set_callback([&]() {
        cnc.go_to_zero(true, true, false);
    });


    jog_btn = jog_btn_holder->add<nanogui::Button>("", FA_ARROW_ALT_CIRCLE_RIGHT);
    jog_btn->set_callback([&]() {
        // FIXME: get rid of std::stof
        cnc.request_jog_fixed(grbl::jog_direction::x_up,
                              std::stof(jog_distances[cbo_jog_distance->selected_index()]),
                              std::stof(jog_feed_rates[cbo_jog_feed_rates->selected_index()]));
    });

    jog_btn_holder->add<nanogui::Label>("");
    jog_btn = jog_btn_holder->add<nanogui::Button>("", FA_ARROW_ALT_CIRCLE_DOWN);
    jog_btn->set_callback([&]() {
        // FIXME: get rid of std::stof
        cnc.request_jog_fixed(grbl::jog_direction::y_down,
                              std::stof(jog_distances[cbo_jog_distance->selected_index()]),
                              std::stof(jog_feed_rates[cbo_jog_feed_rates->selected_index()]));
    });
    jog_btn_holder->add<nanogui::Label>("");


    // z up/down
    auto jog_z_btn_holder = holder->add<nanogui::Widget>();
    jog_z_btn_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 6));

    jog_btn = jog_z_btn_holder->add<nanogui::Button>("", FA_CHEVRON_UP);
    jog_btn->set_callback([&]() {
        // FIXME: get rid of std::stof
        cnc.request_jog_fixed(grbl::jog_direction::z_up,
                              std::stof(jog_distances[cbo_jog_distance->selected_index()]),
                              std::stof(jog_feed_rates[cbo_jog_feed_rates->selected_index()]));
    });

    jog_btn = jog_z_btn_holder->add<nanogui::Button>("", FA_HOME);
    jog_btn->set_callback([&]() {
        cnc.go_to_zero(false, false, true);
    });

    jog_btn = jog_z_btn_holder->add<nanogui::Button>("", FA_CHEVRON_DOWN);
    jog_btn->set_callback([&]() {
        // FIXME: get rid of std::stof
        cnc.request_jog_fixed(grbl::jog_direction::z_down,
                              std::stof(jog_distances[cbo_jog_distance->selected_index()]),
                              std::stof(jog_feed_rates[cbo_jog_feed_rates->selected_index()]));
    });
}

void SenderApp::add_dro_markup() {
    dro_ss << std::setprecision(3) << std::fixed;

    // DRO
    info_layer->add<nanogui::Label>("DRO", "sans-bold", 20);
    auto dro_holder = info_layer->add<nanogui::Widget>();
    dro_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 1, nanogui::Alignment::Middle, 0, 6));

    auto x_holder = dro_holder->add<nanogui::Widget>();
    x_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 0, 6));
    auto lbl = x_holder->add<nanogui::Label>("X", "sans-bold", 20);
    lbl->set_fixed_width(30);
    mpos_x_text = x_holder->add<nanogui::TextBox>(std::to_string(cnc.get_status().machine_pos[0]));
    mpos_x_text->set_fixed_width(200);
    auto zero_btn = x_holder->add<nanogui::ToolButton>(FA_BAN);
    zero_btn->set_flags(nanogui::Button::NormalButton);
    zero_btn->set_tooltip("Zero axis");
    zero_btn->set_callback([&]() {
        cnc.zero_offset_axis(cbo_work_offset->selected_index(), 0);
    });

    auto y_holder = dro_holder->add<nanogui::Widget>();
    y_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 0, 6));
    lbl = y_holder->add<nanogui::Label>("Y", "sans-bold", 20);
    lbl->set_fixed_width(30);
    mpos_y_text = y_holder->add<nanogui::TextBox>(std::to_string(cnc.get_status().machine_pos[1]));
    mpos_y_text->set_fixed_width(200);
    zero_btn = y_holder->add<nanogui::ToolButton>(FA_BAN);
    zero_btn->set_flags(nanogui::Button::NormalButton);
    zero_btn->set_tooltip("Zero axis");
    zero_btn->set_callback([&]() {
        cnc.zero_offset_axis(cbo_work_offset->selected_index(), 1);
    });

    auto z_holder = dro_holder->add<nanogui::Widget>();
    z_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 0, 6));
    lbl = z_holder->add<nanogui::Label>("Z", "sans-bold", 20);
    lbl->set_fixed_width(30);
    mpos_z_text = z_holder->add<nanogui::TextBox>(std::to_string(cnc.get_status().machine_pos[0]));
    mpos_z_text->set_fixed_width(200);
    zero_btn = z_holder->add<nanogui::ToolButton>(FA_BAN);
    zero_btn->set_flags(nanogui::Button::NormalButton);
    zero_btn->set_tooltip("Zero axis");
    zero_btn->set_callback([&]() {
        cnc.zero_offset_axis(cbo_work_offset->selected_index(), 2);
    });

    auto axis_btn_holder = dro_holder->add<Widget>();
    axis_btn_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 0, 6));

    auto zero_all_btn = axis_btn_holder->add<nanogui::Button>("Zero all");
    zero_all_btn->set_callback([&]() {
        cnc.zero_offset(cbo_work_offset->selected_index());
    });

    auto goto_zero_btn = axis_btn_holder->add<nanogui::Button>("Goto 0");
    goto_zero_btn->set_callback([&]() {
        cnc.go_to_zero(true, true, true);
    });
}

void SenderApp::add_program_extents() {
    dro_ss << std::setprecision(3) << std::fixed;

    // Program extents
    info_layer->add<nanogui::Label>("Program extents", "sans-bold", 20);
    auto extents_holder = info_layer->add<nanogui::Widget>();
    extents_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 3, nanogui::Alignment::Fill, 4, 4));

    extents_holder->add<nanogui::Label>("X", "sans-bold", 20);
    extents_min_x = extents_holder->add<nanogui::TextBox>("");
    extents_max_x = extents_holder->add<nanogui::TextBox>("");

    extents_holder->add<nanogui::Label>("Y", "sans-bold", 20);
    extents_min_y = extents_holder->add<nanogui::TextBox>("");
    extents_max_y = extents_holder->add<nanogui::TextBox>("");

    extents_holder->add<nanogui::Label>("Z", "sans-bold", 20);
    extents_min_z = extents_holder->add<nanogui::TextBox>("");
    extents_max_z = extents_holder->add<nanogui::TextBox>("");
}

void SenderApp::save_heightmap(const grbl::heightmap &grid, std::string path) {
    std::ofstream out(path);
    if (out) {
        out << "from_x:" << grid.from_x << std::endl;
        out << "from_y:" << grid.from_y << std::endl;
        out << "to_x:" << grid.to_x << std::endl;
        out << "to_y:" << grid.to_y << std::endl;
        out << "resolution:" << grid.resolution << std::endl;
        out << "probes:" << grid.vertices.size() << std::endl;
        for (auto &v: grid.vertices) {
            out << v.x << ":" << v.y << ":" << v.z << std::endl;
        }
    }
}

grbl::heightmap SenderApp::load_heightmap(std::string path) {
    grbl::heightmap result{};

    std::ifstream in_file(path);
    if (in_file) {
        float from_x, from_y;
        float to_x, to_y;
        float resolution;
        size_t probe_location = 0;

        for (std::string line; std::getline(in_file, line);) {
            auto pieces = split_string(line, ":");
            if (pieces[0] == "from_x") {
                from_x = std::stof(pieces[1]);
            } else if (pieces[0] == "from_y") {
                from_y = std::stof(pieces[1]);
            } else if (pieces[0] == "to_x") {
                to_x = std::stof(pieces[1]);
            } else if (pieces[0] == "to_y") {
                to_y = std::stof(pieces[1]);
            } else if (pieces[0] == "resolution") {
                resolution = std::stof(pieces[1]);
            } else if (pieces[0] == "probes") {
                result = grbl::heightmap::from_params(from_x, from_y, to_x, to_y, resolution);
            } else {
                result.vertices[probe_location].x = std::stof(pieces[0]);
                result.vertices[probe_location].y = std::stof(pieces[1]);
                result.vertices[probe_location].z = std::stof(pieces[2]);
                probe_location++;
            }
        }
    }

    return result;
}

void SenderApp::fill_heightmap_controls_from_grid(const grbl::heightmap &grid) const {
    txt_heightmap_from_x->set_value(std::to_string(grid.from_x));
    txt_heightmap_from_y->set_value(std::to_string(grid.from_y));
    txt_heightmap_to_x->set_value(std::to_string(grid.to_x));
    txt_heightmap_to_y->set_value(std::to_string(grid.to_y));
    txt_heightmap_res->set_value(std::to_string(grid.resolution));
}

void SenderApp::add_heightmap_markup() {
    heightmap_layer->add<nanogui::Label>("Grid definition", "sans-bold", 20);
    auto heightmap_params_holder = heightmap_layer->add<nanogui::Widget>();
    heightmap_params_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 4, 4));

    // grid size
    auto grid_size_holder = heightmap_params_holder->add<Widget>();
    grid_size_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 3, nanogui::Alignment::Middle, 4, 4));

    grid_size_holder->add<nanogui::Label>("", "sans-bold", 20);
    grid_size_holder->add<nanogui::Label>("X", "sans-bold", 20);
    grid_size_holder->add<nanogui::Label>("Y", "sans-bold", 20);

    grid_size_holder->add<nanogui::Label>("From", "sans-bold", 20);
    txt_heightmap_from_x = grid_size_holder->add<nanogui::TextBox>("");
    txt_heightmap_from_x->set_editable(true);
    txt_heightmap_from_x->set_fixed_width(150);
    txt_heightmap_from_y = grid_size_holder->add<nanogui::TextBox>("");
    txt_heightmap_from_y->set_editable(true);
    txt_heightmap_from_y->set_fixed_width(150);

    grid_size_holder->add<nanogui::Label>("To", "sans-bold", 20);
    txt_heightmap_to_x = grid_size_holder->add<nanogui::TextBox>("");
    txt_heightmap_to_x->set_editable(true);
    txt_heightmap_to_x->set_fixed_width(150);
    txt_heightmap_to_y = grid_size_holder->add<nanogui::TextBox>("");
    txt_heightmap_to_y->set_editable(true);
    txt_heightmap_to_y->set_fixed_width(150);

    grid_size_holder->add<nanogui::Label>("");
    auto btn = grid_size_holder->add<nanogui::Button>("Fetch from model");
    btn->set_callback([&]() {
        fill_heightmap_from_model();
    });
    auto btn_load_heightmap = grid_size_holder->add<nanogui::Button>("Load from file");
    btn_load_heightmap->set_callback([&]() {
        auto path = nanogui::file_dialog(
                {{"hmap", "Heightmap files"}
                }, true);
        if (!path.empty()) {
            heightmap_grid = load_heightmap(path);
            fill_heightmap_controls_from_grid(heightmap_grid);
            renderer.update_grid(heightmap_grid, std::stof(exaggeration_factors[cbo_exaggeration->selected_index()]));
        }
    });

    txt_heightmap_from_x->set_callback([&](const std::string &new_value) {
        update_grid();
        return true;
    });
    txt_heightmap_from_y->set_callback([&](const std::string &new_value) {
        update_grid();
        return true;
    });

    txt_heightmap_to_x->set_callback([&](const std::string &new_value) {
        update_grid();
        return true;
    });
    txt_heightmap_to_y->set_callback([&](const std::string &new_value) {
        update_grid();
        return true;
    });


    // grid resolution
    auto grid_res_holder = heightmap_params_holder->add<Widget>();
    grid_res_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 4, 4));
    grid_res_holder->add<nanogui::Label>("Grid resolution", "sans-bold", 20);
    txt_heightmap_res = grid_res_holder->add<nanogui::TextBox>("5");
    txt_heightmap_res->set_editable(true);
    txt_heightmap_res->set_fixed_width(150);
    txt_heightmap_res->set_callback([&](const std::string &new_value) {
        update_grid();
        return true;
    });
    grid_res_holder->add<nanogui::Label>("mm", "sans-bold", 20);

    // business params
    auto heightmap_business_holder = heightmap_params_holder->add<nanogui::Widget>();
    heightmap_business_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 3, nanogui::Alignment::Fill, 4, 4));

    heightmap_business_holder->add<nanogui::Label>("Clearance height Z", "sans-bold", 20);
    txt_clearance_height = heightmap_business_holder->add<nanogui::TextBox>("1.5");
    txt_clearance_height->set_editable(true);
    heightmap_business_holder->add<nanogui::Label>("", "sans-bold");

    heightmap_business_holder->add<nanogui::Label>("Start probing at Z", "sans-bold", 20);
    txt_start_probing_at = heightmap_business_holder->add<nanogui::TextBox>("0.5");
    txt_start_probing_at->set_editable(true);
    heightmap_business_holder->add<nanogui::Label>("", "sans-bold");

    heightmap_business_holder->add<nanogui::Label>("Max negative Z", "sans-bold", 20);
    txt_max_negative_z = heightmap_business_holder->add<nanogui::TextBox>("-1.0");
    txt_max_negative_z->set_editable(true);
    heightmap_business_holder->add<nanogui::Label>("(when to fail probing)", "sans-bold");

    heightmap_business_holder->add<nanogui::Label>("Final Z safety height", "sans-bold", 20);
    txt_final_z_height = heightmap_business_holder->add<nanogui::TextBox>("15");
    txt_final_z_height->set_editable(true);
    heightmap_business_holder->add<nanogui::Label>("", "sans-bold");

    heightmap_business_holder->add<nanogui::Label>("", "sans-bold");
    auto btn_start_probing = heightmap_business_holder->add<nanogui::Button>("Start Probing");
    btn_start_probing->set_callback([&]() {
        cnc.probe_heightmap(heightmap_grid);
    });
    auto btn_save_heightmap = heightmap_business_holder->add<nanogui::Button>("Save heightmap");
    btn_save_heightmap->set_callback([&]() {
        auto path = nanogui::file_dialog(
                {{"hmap", "Heightmap files"}
                }, true);
        if (!path.empty()) {
            save_heightmap(heightmap_grid, path);
        }
    });

    heightmap_layer->add<nanogui::Label>("Grid rendering", "sans-bold", 20);
    auto heightmap_render_holder = heightmap_layer->add<nanogui::Widget>();
    heightmap_render_holder->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 2, nanogui::Alignment::Fill, 4, 4));


    heightmap_render_holder->add<nanogui::Label>("Exaggeration factor");
    cbo_exaggeration = heightmap_render_holder->add<nanogui::ComboBox>(exaggeration_factors);
    cbo_exaggeration->set_callback([&](int idx) {
        renderer.update_grid(heightmap_grid, std::stof(exaggeration_factors[cbo_exaggeration->selected_index()]));
    });

}

void SenderApp::fill_heightmap_from_model() const {
    auto min = renderer.get_extents_min();
    auto max = renderer.get_extents_max();
    txt_heightmap_from_x->set_value(std::to_string(min.x));
    txt_heightmap_from_y->set_value(std::to_string(min.y));
    txt_heightmap_to_x->set_value(std::to_string(max.x));
    txt_heightmap_to_y->set_value(std::to_string(max.y));
}

void SenderApp::update_grid() {
    auto from_x = std::stof(txt_heightmap_from_x->value());
    auto from_y = std::stof(txt_heightmap_from_y->value());
    auto to_x = std::stof(txt_heightmap_to_x->value());
    auto to_y = std::stof(txt_heightmap_to_y->value());
    auto res = std::stof(txt_heightmap_res->value());

    if (from_x != heightmap_grid.from_x ||
        from_y != heightmap_grid.from_y ||
        to_x != heightmap_grid.to_x ||
        to_y != heightmap_grid.to_y ||
        res != heightmap_grid.resolution) {
        heightmap_grid = std::move(grbl::heightmap::from_params(from_x, from_y, to_x, to_y, res));
        renderer.update_grid(heightmap_grid, std::stof(exaggeration_factors[cbo_exaggeration->selected_index()]));
    }
}

void SenderApp::add_pins_markup() {
    // Pins
    info_layer->add<nanogui::Label>("Pins", "sans-bold", 20);
    auto pins_holder = info_layer->add<nanogui::Widget>();
    pins_holder->set_layout(
            new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Fill, 4, 4));

    btn_pin_door = pins_holder->add<nanogui::ToolButton>(0, "D");
//        btn_pin_door->set_enabled(false);
    btn_pin_door->set_flags(nanogui::Button::Flags::NormalButton);

    btn_pin_hold = pins_holder->add<nanogui::ToolButton>(0, "H");
//        btn_pin_hold->set_enabled(false);
    btn_pin_hold->set_flags(nanogui::Button::Flags::NormalButton);

    btn_pin_reset = pins_holder->add<nanogui::ToolButton>(0, "R");
//        btn_pin_reset->set_enabled(false);
    btn_pin_reset->set_flags(nanogui::Button::Flags::NormalButton);

    btn_pin_cycle_start = pins_holder->add<nanogui::ToolButton>(0, "S");
//        btn_pin_cycle_start->set_enabled(false);
    btn_pin_cycle_start->set_flags(nanogui::Button::Flags::NormalButton);

    auto a = pins_holder->add<nanogui::Label>("");
    a->set_fixed_width(30);

    btn_pin_limit_x = pins_holder->add<nanogui::ToolButton>(0, "X");
//        btn_pin_limit_x->set_enabled(false);
    btn_pin_limit_x->set_flags(nanogui::Button::Flags::NormalButton);

    btn_pin_limit_y = pins_holder->add<nanogui::ToolButton>(0, "Y");
//        btn_pin_limit_y->set_enabled(false);
    btn_pin_limit_y->set_flags(nanogui::Button::Flags::NormalButton);

    btn_pin_limit_z = pins_holder->add<nanogui::ToolButton>(0, "Z");
//        btn_pin_limit_z->set_enabled(false);
    btn_pin_limit_z->set_flags(nanogui::Button::Flags::NormalButton);

    btn_pin_probe = pins_holder->add<nanogui::ToolButton>(0, "P");
//        btn_pin_probe->set_enabled(false);
    btn_pin_probe->set_flags(nanogui::Button::Flags::NormalButton);
}

void SenderApp::refresh_offset() {
    auto offset_name = "G" + std::to_string(cbo_work_offset->selected_index() + 54);
    cnc.set_work_offset(offset_name);
}

void SenderApp::fill_in_parameters() {
    parameters_vscroll = new nanogui::VScrollPanel(tab_widget);
    tab_widget->append_tab("Parameters", parameters_vscroll);

    parameters_layer = new Widget(parameters_vscroll);
    parameters_layer->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 1, nanogui::Alignment::Middle));

    auto &parameters = cnc.get_parameters();

    for (auto &entry: parameters) {
        auto w = parameters_layer->add<Widget>();
        w->set_layout(new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 2, 2));
        auto x = w->add<nanogui::Label>(entry.first, "sans-bold", 20);
        x->set_fixed_width(50);

        auto y = w->add<nanogui::TextBox>(entry.second);
        y->set_editable(true);
        y->set_fixed_width(200);

        auto z = w->add<nanogui::Button>("", FA_SAVE);
//            z->set_flags(Button::Flags::NormalButton); // no toggle, please
        z->set_tooltip("save");
    }
}

void SenderApp::fill_in_settings() {
    settings_vscroll = new nanogui::VScrollPanel(tab_widget);
    tab_widget->append_tab("Settings", settings_vscroll);

    settings_layer = new nanogui::Widget(settings_vscroll);
    settings_layer->set_layout(
            new nanogui::GridLayout(nanogui::Orientation::Horizontal, 1, nanogui::Alignment::Middle));

    auto &settings = cnc.get_settings();

    for (auto &s: settings) {
        auto w = settings_layer->add<nanogui::Widget>();
        w->set_layout(new nanogui::BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 2, 2));
        auto x = w->add<nanogui::Label>(s.first, "sans-bold", 20);
        x->set_fixed_width(40);

        auto desc = grbl::setting_description(std::stoi(s.first.substr(1)));

        auto y = w->add<nanogui::TextBox>(s.second);
        y->set_editable(true);
        y->set_tooltip(desc);
        y->set_fixed_width(150);

        auto z = w->add<nanogui::ToolButton>(FA_SAVE);
        z->set_flags(nanogui::Button::Flags::NormalButton); // no toggle, please
        z->set_tooltip("save");

        auto t = w->add<nanogui::TextArea>();
        t->set_fixed_width(200);
        t->set_fixed_height(20);
        t->clear();
        t->append(desc.substr(0, desc.find_first_of('\n')));
    }
}

void SenderApp::init_program_geometry() {
    renderer.update(pgm, cnc);

    auto max_pos = renderer.get_extents_max();
    auto min_pos = renderer.get_extents_min();

    cam_target = (max_pos - min_pos) / 2.0f;
    cam_zoom = (max_pos.x - min_pos.x);
    cam_src_rotation = glm::quat(1.0, 0.0, 0.0, 0.0);
}

void SenderApp::set_program_extents() const {
    auto min = renderer.get_extents_min();
    auto max = renderer.get_extents_max();

    extents_min_x->set_value(std::to_string(min.x));
    extents_max_x->set_value(std::to_string(max.x));

    extents_min_y->set_value(std::to_string(min.y));
    extents_max_y->set_value(std::to_string(max.y));

    extents_min_z->set_value(std::to_string(min.z));
    extents_max_z->set_value(std::to_string(max.z));
}

bool SenderApp::resize_event(const nanogui::Vector2i &size) {
//        window->set_fixed_height(this->height() / 2);
    tab_widget->set_fixed_height((this->height() - 30));
    perform_layout();

    return Screen::resize_event(size);
}

void SenderApp::update_dro() {
    auto work_pos = cnc.get_work_pos();

    dro_ss.str("");
    dro_ss << work_pos[0];
    mpos_x_text->set_value(dro_ss.str());

    dro_ss.str("");
    dro_ss << work_pos[1];
    mpos_y_text->set_value(dro_ss.str());

    dro_ss.str("");
    dro_ss << work_pos[2];
    mpos_z_text->set_value(dro_ss.str());
}

bool SenderApp::keyboard_event(int key, int scancode, int action, int modifiers) {
    if (Screen::keyboard_event(key, scancode, action, modifiers))
        return true;

//        if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
//            set_visible(false);
//            return true;
//        }

    if (key == GLFW_KEY_SPACE && action == GLFW_PRESS && !window->mouse_focused()) {
        // reset trackball rotation
        cam_src_rotation = glm::quat(1, 0, 0, 0);
    }

    auto new_jog = jog;

    if (key == GLFW_KEY_LEFT_SHIFT) {
        if (action == GLFW_PRESS) {
            new_jog.speed_fast_pressed = true;
        } else if (action == GLFW_RELEASE) {
            new_jog.speed_fast_pressed = false;
        }
    }

    if (key == GLFW_KEY_LEFT_CONTROL) {
        if (action == GLFW_PRESS) {
            new_jog.speed_slow_pressed = true;
        } else if (action == GLFW_RELEASE) {
            new_jog.speed_slow_pressed = false;
        }
    }

    if (key == GLFW_KEY_UP) {
        if (action == GLFW_PRESS) {
            new_jog.up_pressed = true;
        } else if (action == GLFW_RELEASE) {
            new_jog.up_pressed = false;
        }
    }

    if (key == GLFW_KEY_DOWN) {
        if (action == GLFW_PRESS) {
            new_jog.down_pressed = true;
        } else if (action == GLFW_RELEASE) {
            new_jog.down_pressed = false;
        }
    }

    if (key == GLFW_KEY_LEFT) {
        if (action == GLFW_PRESS) {
            new_jog.left_pressed = true;
        } else if (action == GLFW_RELEASE) {
            new_jog.left_pressed = false;
        }
    }

    if (key == GLFW_KEY_RIGHT) {
        if (action == GLFW_PRESS) {
            new_jog.right_pressed = true;
        } else if (action == GLFW_RELEASE) {
            new_jog.right_pressed = false;
        }
    }

    if (key == GLFW_KEY_PAGE_UP) {
        if (action == GLFW_PRESS) {
            new_jog.z_up_pressed = true;
        } else if (action == GLFW_RELEASE) {
            new_jog.z_up_pressed = false;
        }
    }

    if (key == GLFW_KEY_PAGE_DOWN) {
        if (action == GLFW_PRESS) {
            new_jog.z_down_pressed = true;
        } else if (action == GLFW_RELEASE) {
            new_jog.z_down_pressed = false;
        }
    }

    if (btn_keyboard_jog->pushed() && jog != new_jog) {
        cnc.request_jog(new_jog);
        jog = new_jog;
    }

    return false;
}

bool
SenderApp::mouse_motion_event(const nanogui::Vector2i &p, const nanogui::Vector2i &rel, int button, int modifiers) {
    auto result = Widget::mouse_motion_event(p, rel, button, modifiers);
    if (window->mouse_focused()) {
        return result;
    } else {
//            std::cout << "Mouse motion p:" << p << ", rel:" << rel << ", button:" << button << ", .modifiers:" << modifiers << std::endl;
        bool is_rotating = button == 1;
        bool is_panning = button == 2;
        if (is_rotating) {
            if (abs(rel.x()) > abs(rel.y())) {
                cam_src_rotation *= glm::angleAxis(rel.x() / 100.0f, glm::vec3(0, 1, 0));
            } else {
                cam_src_rotation *= glm::angleAxis(rel.y() / 100.0f, glm::vec3(1, 0, 0));
            }

        } else if (is_panning) {
            cam_pan.x += (float) rel.x() / 10.0f;
            cam_pan.y -= (float) rel.y() / 10.0f;
        }
        return true;
    }
}

bool SenderApp::scroll_event(const nanogui::Vector2i &p, const nanogui::Vector2f &rel) {
    if (window->mouse_focused()) {
        return Widget::scroll_event(p, rel);
    } else {
//            std::cout << "Scroll event: p:" << p << ", rel:" << rel << std::endl;
        cam_zoom -= rel.y() * cam_zoom / 10.0f;
        cam_zoom = std::max(cam_zoom, 0.1f);

        return true;
    }
}

void SenderApp::draw(NVGcontext *ctx) {
    std::shared_ptr<grbl::machine_event> event;
    while ((event = cnc.pop_event()) != nullptr) {
        switch (event->type) {
            case grbl::machine_event_type::connected:
                break;
            case grbl::machine_event_type::disconnected:
                break;
            case grbl::machine_event_type::report_received: {

                auto ev = dynamic_cast<grbl::machine_event_report_received *>(event.get());

                txt_status->set_value(grbl::status_to_string(cnc.get_status().status));

                if (cnc.get_status().status == grbl::machine_status::alarm) {
                    txt_status->set_tooltip(grbl::alarm_to_string(last_alarm));
                } else {
                    txt_status->set_tooltip(cnc.get_status().sub_status);
                }

                // FIXME: ugly way of retrieving the bg color
                btn_pin_door->set_background_color(
                        ev->report.signals.bit.door ? color_red : btn_load_program->background_color());
                btn_pin_hold->set_background_color(
                        ev->report.signals.bit.hold ? color_red : btn_load_program->background_color());
                btn_pin_reset->set_background_color(
                        ev->report.signals.bit.soft_reset ? color_red : btn_load_program->background_color());
                btn_pin_cycle_start->set_background_color(
                        ev->report.signals.bit.cycle_start ? color_red : btn_load_program->background_color());
                btn_pin_limit_x->set_background_color(
                        ev->report.signals.bit.x_limit ? color_red : btn_load_program->background_color());
                btn_pin_limit_y->set_background_color(
                        ev->report.signals.bit.y_limit ? color_red : btn_load_program->background_color());
                btn_pin_limit_z->set_background_color(
                        ev->report.signals.bit.z_limit ? color_red : btn_load_program->background_color());
                btn_pin_probe->set_background_color(
                        ev->report.signals.bit.probe ? color_red : btn_load_program->background_color());

                update_dro();
                last_report = ev->report;
                break;
            }
            case grbl::machine_event_type::banner:
                break;
            case grbl::machine_event_type::message: {
                auto ev = dynamic_cast<grbl::machine_event_message *>(event.get());
                txt_message->set_value(ev->message);
                set_caption(ev->message);
                break;
            }
            case grbl::machine_event_type::alarm: {
                auto ev = dynamic_cast<grbl::machine_event_alarm *>(event.get());
                last_alarm = ev->alarm;
                break;
            }
            case grbl::machine_event_type::init_completed:
                fill_in_settings();
                fill_in_parameters();

                perform_layout();

                refresh_offset();
                update_dro();
                break;
            case grbl::machine_event_type::run_completed: {
                auto ev = dynamic_cast<grbl::machine_event_run_completed *>(event.get());
                btn_run_program->set_background_color(ev->success ? color_green : color_red);
                if (ev->success) {
                    new nanogui::MessageDialog(this, nanogui::MessageDialog::Type::Information, "Run result",
                                               "Program executed successfully");
                } else {
                    std::stringstream ss;
                    auto i = pgm.instruction_at(ev->failed_index);
                    ss << "Program execution failed at line " << i.line << " (" << i.command << "). Error: "
                       << ev->error << ", "
                       << grbl::error_to_string(ev->error);
                    new nanogui::MessageDialog(this, nanogui::MessageDialog::Type::Warning, "Run result", ss.str());
                }
                break;
            }
            case grbl::machine_event_type::check_completed: {
                auto ev = dynamic_cast<grbl::machine_event_check_completed *>(event.get());

                btn_check_program->set_background_color(ev->success ? color_green : color_red);
                if (ev->success) {
                    btn_run_program->set_enabled(true);
                    new nanogui::MessageDialog(this, nanogui::MessageDialog::Type::Information, "Check result",
                                               "Program checked successfully");
                } else {
                    btn_run_program->set_enabled(false);
                    std::stringstream ss;
                    auto i = pgm.instruction_at(ev->failed_index);
                    ss << "Program check failed at line " << i.line << " (" << i.command << "). Error: "
                       << ev->error << ", "
                       << grbl::error_to_string(ev->error);
                    new nanogui::MessageDialog(this, nanogui::MessageDialog::Type::Warning, "Check result",
                                               ss.str());
                }
                break;
            }
            case grbl::machine_event_type::settings_reloaded:
                break;
            case grbl::machine_event_type::parameters_reloaded:
                refresh_offset();
                update_dro();
                break;
            case grbl::machine_event_type::probe_result: {
                auto ev = dynamic_cast<grbl::machine_event_probe_result *>(event.get());

                std::stringstream ss;
                ss << "Probing ended. Result: " << std::boolalpha << ev->probe_touched << " at coords: ";
                for (float probe_coord: ev->probe_coords) {
                    ss << probe_coord << ", ";
                }
                new nanogui::MessageDialog(this, nanogui::MessageDialog::Type::Warning, "Probe result", ss.str());
                break;
            }
            case grbl::machine_event_type::heightmap_probe_acquired: {
                std::cout << "Updating grid" << std::endl;
                auto ev = dynamic_cast<grbl::machine_event_heightmap_probe_acquired *>(event.get());
                renderer.update_grid(*ev->grid, std::stof(exaggeration_factors[cbo_exaggeration->selected_index()]));
                break;
            }
        }
    }

    Widget::draw(ctx);
}

void SenderApp::draw_contents() {
    auto fb_size = framebuffer_size();

    // start rendering
    m_render_pass->resize(framebuffer_size());
    m_render_pass->begin();

    renderer.update(pgm, cnc);
    // compute mvp
    glm::mat4 projection = glm::perspective(45.0f * glm::pi<float>() / 180.0f,
                                            (float) fb_size.x() / (float) fb_size.y(),
                                            0.1f,
                                            10000.f);

    glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, -cam_zoom)) *
                     glm::toMat4(cam_src_rotation) *
                     glm::translate(glm::mat4(1.0f), cam_pan - cam_target);

    glm::mat4 model = glm::mat4(1.0f);
    glm::mat3 normal_mat = glm::inverseTranspose(glm::mat3(view * model));

    renderer.render(model, view, projection, normal_mat, glm::vec2(fb_size.x(), fb_size.y()));

    m_render_pass->end();
}
