/*
    src/example1.cpp -- C++ version of an example application that shows
    how to use the various widget classes. For a Python implementation, see
    '../python/example1.py'.

    NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>.
    The widget drawing code is based on the NanoVG demo application
    by Mikko Mononen.

    All rights reserved. Use of this source code is governed by a
    BSD-style license that can be found in the LICENSE.txt file.
*/

#include <nanogui/opengl.h>
#include <nanogui/screen.h>
#include <nanogui/window.h>
#include <nanogui/layout.h>
#include <nanogui/label.h>
#include <nanogui/checkbox.h>
#include <nanogui/button.h>
#include <nanogui/toolbutton.h>
#include <nanogui/popupbutton.h>
#include <nanogui/combobox.h>
#include <nanogui/progressbar.h>
#include <nanogui/icons.h>
#include <nanogui/messagedialog.h>
#include <nanogui/textbox.h>
#include <nanogui/slider.h>
#include <nanogui/imagepanel.h>
#include <nanogui/imageview.h>
#include <nanogui/vscrollpanel.h>
#include <nanogui/colorwheel.h>
#include <nanogui/colorpicker.h>
#include <nanogui/graph.h>
#include <nanogui/tabwidget.h>
#include <nanogui/texture.h>
#include <nanogui/shader.h>
#include <nanogui/renderpass.h>
#include <iostream>
#include <memory>

#define STB_IMAGE_STATIC
#define STB_IMAGE_IMPLEMENTATION
#if defined(_MSC_VER)
#  pragma warning (disable: 4505) // don't warn about dead code in stb_image.h
#elif defined(__GNUC__)
#   pragma GCC diagnostic ignored "-Wunused-function"
#endif

#include <stb_image.h>
#include "grbl.h"
#include <gtest/gtest.h>
#include "grbl_communication.h"
#include "machine.h"

using namespace nanogui;

grbl::machine cnc{};


class SenderApp : public Screen, public grbl::machine_listener {
public:

    Window *window;
    grbl::jog_state jog;
    Label *m_pos_x, *m_pos_y, *m_pos_z;
    TextBox *lblStatus, *lblSubstatus;
    nanogui::Color red = nanogui::Color(255, 0, 0, 255);
    int last_alarm = 0;

    SenderApp() : Screen(Vector2i(1024, 768), "GRBL Sender") {
        inc_ref();
        window = new Window(this, "Machine status");
//        window->set_fixed_height(Screen::size().y());
        window->set_position(Vector2i(0, 0));
        window->set_layout(new GroupLayout());
//        window->set_size(Screen::size());

        new Label(window, "Status", "sans-bold");
        Widget *status_holder = new Widget(window);
        status_holder->set_layout(new GridLayout());

        lblStatus = new TextBox(window, grbl::status_to_string(cnc.get_status().status));
        lblSubstatus = new TextBox(window, cnc.get_status().sub_status);

        // Machine pos
        new Label(window, "Machine pos", "sans-bold");
        Widget *mpos = new Widget(window);
        mpos->set_layout(new GridLayout());

        new Label(mpos, "X");
        m_pos_x = new Label(mpos, std::to_string(cnc.get_status().machine_pos[0]));
        new Label(mpos, "Y");
        m_pos_y = new Label(mpos, std::to_string(cnc.get_status().machine_pos[1]));
        new Label(mpos, "Z");
        m_pos_z = new Label(mpos, std::to_string(cnc.get_status().machine_pos[2]));

        // buttons to change state
        new Label(window, "Actions", "sans-bold");
        Widget *actions = new Widget(window);
        actions->set_layout(new BoxLayout(Orientation::Horizontal));

        Button *btnUnlock = new Button(actions, "Unlock");
        btnUnlock->set_callback([&] {
            cnc.request_unlock();
        });
        Button *btnHome = new Button(actions, "Home");
        btnHome->set_callback([&] {
            cnc.request_home();
        });
        Button *btnReset = new Button(actions, "Reset");
        btnReset->set_background_color(red);
        btnReset->set_callback([&] {
            cnc.request_reset();
        });
        Button *btnCycleStart = new Button(actions, "Cycle Start");
        btnCycleStart->set_callback([&] {
            cnc.request_cycle_start();
        });
        Button *btnFeedHold = new Button(actions, "Feed Hold");
        btnFeedHold->set_callback([&] {
            cnc.request_feed_hold();
        });

        // No need to store a pointer, the data structure will be automatically
        // freed when the parent window is deleted
        new Label(window, "Program", "sans-bold");

        Button *btnLoadProgram = new Button(window, "Load");
        btnLoadProgram->set_callback([&] {
            auto path = file_dialog(
                    {{"nc",  "G-Code files"},
                     {"ngc", "G-Code files"}}, false);
            grbl::program pgm{path};
            if (pgm.is_loaded) {
                cnc.run_program(pgm);
            }
        });
        btnLoadProgram->set_tooltip("short tooltip");

        // Alternative construction notation using variadic template
        btnLoadProgram = window->add<Button>("Styled", FA_ROCKET);
        btnLoadProgram->set_background_color(Color(0, 0, 255, 25));
        btnLoadProgram->set_callback([] { std::cout << "pushed!" << std::endl; });
        btnLoadProgram->set_tooltip("This button has a fairly long tooltip. It is so long, in "
                                    "fact, that the shown text will span several lines.");

        new Label(window, "Toggle buttons", "sans-bold");
        btnLoadProgram = new Button(window, "Toggle me");
        btnLoadProgram->set_flags(Button::ToggleButton);
        btnLoadProgram->set_change_callback([](bool state) { std::cout << "Toggle button state: " << state << std::endl; });

        new Label(window, "Radio buttons", "sans-bold");
        btnLoadProgram = new Button(window, "Radio button 1");
        btnLoadProgram->set_flags(Button::RadioButton);
        btnLoadProgram = new Button(window, "Radio button 2");
        btnLoadProgram->set_flags(Button::RadioButton);

        new Label(window, "A tool palette", "sans-bold");
        Widget *tools = new Widget(window);
        tools->set_layout(new BoxLayout(Orientation::Horizontal,
                                        Alignment::Middle, 0, 6));

        btnLoadProgram = new ToolButton(tools, FA_CLOUD);
        btnLoadProgram = new ToolButton(tools, FA_FAST_FORWARD);
        btnLoadProgram = new ToolButton(tools, FA_COMPASS);
        btnLoadProgram = new ToolButton(tools, FA_UTENSILS);

        new Label(window, "Popup buttons", "sans-bold");
        PopupButton *popup_btn = new PopupButton(window, "Popup", FA_FLASK);
        Popup *popup = popup_btn->popup();
        popup->set_layout(new GroupLayout());
        new Label(popup, "Arbitrary widgets can be placed here");
        new CheckBox(popup, "A check box");
        // popup right
        popup_btn = new PopupButton(popup, "Recursive popup", FA_CHART_PIE);
        Popup *popup_right = popup_btn->popup();
        popup_right->set_layout(new GroupLayout());
        new CheckBox(popup_right, "Another check box");
        // popup left
        popup_btn = new PopupButton(popup, "Recursive popup", FA_DNA);
        popup_btn->set_side(Popup::Side::Left);
        Popup *popup_left = popup_btn->popup();
        popup_left->set_layout(new GroupLayout());
        new CheckBox(popup_left, "Another check box");

        perform_layout();

//           All NanoGUI widgets are initialized at this point. Now
//           create shaders to draw the main window contents.
//
//           NanoGUI comes with a simple wrapper around OpenGL 3, which
//           eliminates most of the tedious and error-prone shader and buffer
//           object management.

        m_render_pass = new RenderPass({this});
        m_render_pass->set_clear_color(0, Color(0.3f, 0.3f, 0.32f, 1.f));

        m_shader = new Shader(
                m_render_pass,

                /* An identifying name */
                "a_simple_shader",

#if defined(NANOGUI_USE_OPENGL)
                R"(/* Vertex shader */
            #version 330
            uniform mat4 mvp;
            in vec3 position;
            void main() {
                gl_Position = mvp * vec4(position, 1.0);
            })",

                /* Fragment shader */
                R"(#version 330
            out vec4 color;
            uniform float intensity;
            void main() {
                color = vec4(vec3(intensity), 1.0);
            })"
#elif defined(NANOGUI_USE_GLES)
                R"(/* Vertex shader */
            precision highp float;
            uniform mat4 mvp;
            attribute vec3 position;
            void main() {
                gl_Position = mvp * vec4(position, 1.0);
            })",

                /* Fragment shader */
                R"(precision highp float;
            uniform float intensity;
            void main() {
                gl_FragColor = vec4(vec3(intensity), 1.0);
            })"
#elif defined(NANOGUI_USE_METAL)
                R"(using namespace metal;
            struct VertexOut {
                float4 position [[position]];
            };

            vertex VertexOut vertex_main(const device packed_float3 *position,
                                         constant float4x4 &mvp,
                                         uint id [[vertex_id]]) {
                VertexOut vert;
                vert.position = mvp * float4(position[id], 1.f);
                return vert;
            })",

                /* Fragment shader */
                R"(using namespace metal;
            fragment float4 fragment_main(const constant float &intensity) {
                return float4(intensity);
            })"
#endif
        );

        uint32_t indices[3 * 2] = {
                0, 1, 2,
                2, 3, 0
        };

        float positions[3 * 4] = {
                -1.f, -1.f, 0.f,
                1.f, -1.f, 0.f,
                1.f, 1.f, 0.f,
                -1.f, 1.f, 0.f
        };

        m_shader->set_buffer("indices", VariableType::UInt32, {3 * 2}, indices);
        m_shader->set_buffer("position", VariableType::Float32, {4, 3}, positions);
        m_shader->set_uniform("intensity", 0.5f);
    }

    bool resize_event(const Vector2i& size) override {
        return Screen::resize_event(size);
    }

    void on_connected() override {
    }

    void on_disconnected() override {
    }

    void on_realtime_status_report(grbl::realtime_status_report report) override {
        lblStatus->set_value(grbl::status_to_string(cnc.get_status().status) + "   ");
        if (cnc.get_status().status == grbl::machine_status::alarm) {
            lblSubstatus->set_value(grbl::alarm_to_string(last_alarm));
        } else {
            lblSubstatus->set_value(cnc.get_status().sub_status);
        }

        m_pos_x->set_caption(std::to_string(cnc.get_status().machine_pos[0]));
        m_pos_y->set_caption(std::to_string(cnc.get_status().machine_pos[1]));
        m_pos_z->set_caption(std::to_string(cnc.get_status().machine_pos[2]));
    }

    void on_banner(std::string line) override {
    }

    void on_message(std::string message) override {
        set_caption(message);
    }

    void on_alarm(int alarm) override {
        last_alarm = alarm;
    }


    bool keyboard_event(int key, int scancode, int action, int modifiers) override {
        if (Screen::keyboard_event(key, scancode, action, modifiers))
            return true;
        if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
            set_visible(false);
            return true;
        }

        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 (jog != new_jog) {
            cnc.request_jog(new_jog);
            jog = new_jog;
        }

        return false;
    }

    virtual void draw(NVGcontext *ctx) {
        // Animate the scrollbar
//        m_progress->set_value(std::fmod((float) glfwGetTime() / 10, 1.0f));

        // Draw the user interface
        Screen::draw(ctx);
    }

    virtual void draw_contents() {
        Matrix4f mvp = Matrix4f::scale(Vector3f(
                (float) m_size.y() / (float) m_size.x() * 0.25f, 0.25f, 0.25f)) *
                       Matrix4f::rotate(Vector3f(0, 0, 1), (float) glfwGetTime());

        m_shader->set_uniform("mvp", mvp);

        m_render_pass->resize(framebuffer_size());
        m_render_pass->begin();

        m_shader->begin();
        m_shader->draw_array(Shader::PrimitiveType::Triangle, 0, 6, true);
        m_shader->end();

        m_render_pass->end();
    }

private:
    ProgressBar *m_progress;
    ref<Shader> m_shader;
    ref<RenderPass> m_render_pass;

    using ImageHolder = std::unique_ptr<uint8_t[], void (*)(void *)>;
    std::vector<std::pair<ref<Texture>, ImageHolder>> m_images;
    int m_current_image;
};


int main(int argc, char **argv) {

    testing::InitGoogleTest(&argc, argv);
    auto result = RUN_ALL_TESTS();
    if (result) {
        exit(result);
    }

    try {
        nanogui::init();

        // scoped variables
        {
            ref<SenderApp> app = new SenderApp();
            app->dec_ref();
            app->draw_all();
            app->set_visible(true);

            cnc.set_listener(app);
            cnc.connect();

            nanogui::mainloop(1 / 60.f * 1000);
        }

        nanogui::shutdown();
    } catch (const std::exception& e) {
        std::string error_msg = std::string("Caught a fatal error: ") + std::string(e.what());
        std::cerr << error_msg << std::endl;
        return -1;
    } catch (...) {
        std::cerr << "Caught an unknown error!" << std::endl;
        return -2;
    }

    return 0;
}
