#include <mutex>
#include <tuple>
#include <atomic>
#include <thread>
#include <vector>
#include <utility>
#include <gtk/gtk.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include "LuxandFaceSDK.h"

#include <iostream>

// Type alias for a void function pointer used for GTK signal callbacks
typedef void(*void_func_p)();

// Type alias for data passed to the edit name dialog response callback
// Contains: entry widget, tracker handle, and face ID
typedef std::tuple<GtkWidget*, HTracker, long long> response_signal_data_t;

/**
 * Casts a member function pointer to a void function pointer for GTK callbacks.
 * This allows C++ member functions to be used as GTK signal handlers.
 * @param pointer The member function pointer to cast
 * @return A void function pointer compatible with GTK callbacks
 */
template<typename F>
void_func_p cast_to_void_func(F pointer) {
    return *((void (**)())&pointer);
}

/**
 * Structure representing a detected face with its properties.
 */
struct face 
{
    long long id;           // Unique identifier for the tracked face
    char name[65536];       // Name associated with the face (if recognized)
    TFacePosition position; // Position and size of the face in the frame
    std::string status;     // Status message (e.g., liveness check result)
    bool isLive = false;    // Whether the face passed the liveness check
};

/**
 * Main window class that handles camera input, face detection, and recognition.
 * Manages the GTK window, face tracking, liveness detection, and user interaction.
 */
class camera_window {

public:

    /**
     * Constructor that initializes the camera, tracker, and GTK window.
     * Sets up video capture, face detection parameters, and starts the recognition thread.
     * @param app Pointer to the GTK application instance
     */
    explicit camera_window(GtkApplication* app) : _app(app), _is_finished(false) 
    {
        int names_count;
        char** names;
        FSDK_GetCameraList(&names, &names_count);

        int format_count;
        FSDK_VideoFormatInfo* format_info;
        FSDK_GetVideoFormatList(*names, &format_info, &format_count);
        FSDK_SetVideoFormat(*names, *format_info);

        _width = format_info->Width;
        _height = format_info->Height;

        FSDK_OpenVideoCamera(*names, &_camera);
        FSDK_FreeCameraList(names, names_count);
        FSDK_FreeVideoFormatList(format_info);

        _data = new unsigned char[_width * _height * 3];

        _window = gtk_application_window_new(app);
        gtk_window_set_title(GTK_WINDOW(_window), "Live Recognition");

        _drawing_area = gtk_drawing_area_new();
        gtk_widget_set_size_request(_drawing_area, _width, _height);
        gtk_widget_set_halign(_drawing_area, GTK_ALIGN_CENTER);
        gtk_widget_set_valign(_drawing_area, GTK_ALIGN_CENTER);
        gtk_widget_set_events(_drawing_area, GDK_BUTTON_PRESS_MASK | GDK_EXPOSURE_MASK | GDK_POINTER_MOTION_MASK);
        g_signal_connect_swapped(_drawing_area, "draw", cast_to_void_func(&camera_window::update_image), this);
        g_signal_connect_swapped(_drawing_area, "button-press-event", cast_to_void_func(&camera_window::mouse_click), this);
        g_signal_connect_swapped(_drawing_area, "motion-notify-event", cast_to_void_func(&camera_window::mouse_move), this);
        gtk_container_add(GTK_CONTAINER(_window), _drawing_area);

        if (FSDK_LoadTrackerMemoryFromFile(&_tracker, tracker_filename) != FSDKE_OK)
            FSDK_CreateTracker(&_tracker);

        int err = 0; // set realtime face detection parameters
	    FSDK_SetTrackerMultipleParameters(_tracker, "HandleArbitraryRotations=false; DetermineFaceRotationAngle=false; InternalResizeWidth=100; FaceDetectionThreshold=5;", &err);

        FSDK_SetTrackerParameter(_tracker, "DetectLiveness", "true");           // enable liveness
        FSDK_SetTrackerParameter(_tracker, "SmoothAttributeLiveness", "true");  // use smooth minimum function for liveness values
        FSDK_SetTrackerParameter(_tracker, "LivenessFramesCount", "6");         // minimal number of frames required to output liveness attribute

        _recognition_thread = new std::thread([this]() {
            HImage image;
            long long ids[256], count;
            while (!_is_finished) {
                {
                    std::lock_guard<std::mutex> lock(_image_mutex);
                    image = _image;
                    _image = -1;
                }
                if (image < 0) {
                    std::this_thread::sleep_for(std::chrono::seconds(1));
                    continue;
                }
                
                if (FSDK_FeedFrame(_tracker, _camera, image, &count, ids, 256 * sizeof(long long)) != FSDKE_OK) {
                    continue;    
                }
                FSDK_FreeImage(image);
                
                {
                    std::lock_guard<std::mutex> lock(_faces_mutex);
                    _count = count;
                    
                    if (_count > _faces.size())
                        _faces.resize(_count);

                    for (long long i = 0; i < _count; ++i) {
                        _faces[i].id = ids[i];

                        FSDK_GetTrackerFacePosition(_tracker, _camera, ids[i], &_faces[i].position);
                        FSDK_GetAllNames(_tracker, ids[i], _faces[i].name, 65536);

                        _faces[i].isLive = false;

                        std::string statusText = "";

                        float liveness = 0.0f;
                        char attrValues[1024];
                        bool hasLiveness = false;

                        if (FSDK_GetTrackerFacialAttribute(_tracker, _camera, ids[0], "Liveness", attrValues, 1024) == FSDKE_OK) {
                            if (FSDK_GetValueConfidence(attrValues, "Liveness", &liveness) == FSDKE_OK) {
                                hasLiveness = true;
                            }
                        }

                        if (!hasLiveness)
                        {
                            statusText = "";
                        }
                        else if (liveness > 0.5f)
                        {
                            statusText = "Liveness check passed";
                            statusText += " (score: " + std::to_string(liveness) + ")";
                            _faces[i].isLive = true;
                        }
                        else
                        {
                            statusText = "Liveness check failed";
                        }
                        _faces[i].status = statusText;
                    } // if (_count > 0)
                }
            }
        });

        g_idle_add([](void* widget) {
            if (!GTK_IS_WIDGET(widget))
                return 0;
            gtk_widget_queue_draw((GtkWidget*)widget);
            return 1;
        }, _drawing_area);

        gtk_widget_show_all(_window);
    }

    // Delete copy constructor to prevent copying
    camera_window(const camera_window&) = delete;
    
    // Delete copy assignment operator to prevent assignment
    camera_window& operator=(const camera_window&) = delete;

    /**
     * Destructor that cleans up resources.
     * Closes camera, stops recognition thread, saves tracker data, and frees memory.
     */
    ~camera_window() {
        FSDK_CloseVideoCamera(_camera);
        _is_finished = true;
        _recognition_thread->join();
        delete _recognition_thread;
        delete[] _data;
        FSDK_SaveTrackerMemoryToFile(_tracker, tracker_filename);
        FSDK_FreeTracker(_tracker);
    }

private:

    // Epsilon value for floating-point comparisons
    static constexpr auto eps = 1e-10;
    
    // Filename for saving/loading tracker memory
    static constexpr auto tracker_filename = "tracker70";

    HImage _image = -1;                    // Current frame image handle
    long long _count = 0;                  // Number of detected faces
    HTracker _tracker = -1;                // Face tracker handle
    std::vector<face> _faces;              // Vector of detected faces
    std::atomic_bool _is_finished;         // Flag to stop recognition thread
    GtkApplication* _app = nullptr;        // GTK application pointer
    unsigned char* _data = nullptr;        // Buffer for image data
    std::thread* _recognition_thread;      // Thread for face recognition processing
    std::pair<double, double> _mouse_pos;  // Current mouse cursor position
    std::mutex _image_mutex, _faces_mutex; // Mutexes for thread-safe access
    int _width = -1, _height = -1, _camera = -1; // Video dimensions and camera handle
    GtkWidget* _window = nullptr, *_drawing_area = nullptr; // GTK window widgets

    static bool contains(const TFacePosition& pos, const double& x, const double& y) 
    {
        const auto x0 = pos.xc - pos.w / 2.;
        const auto x1 = pos.xc + pos.w / 2.;
        const auto y0 = pos.yc - pos.w * .6;
        const auto y1 = pos.yc + pos.w * .6;
        return x - x0 > -eps && x1 - x > -eps && y - y0 > -eps && y1 - y > -eps;
    }

    /**
     * Callback function that updates and draws the camera image with face overlays.
     * Grabs a new frame, draws it to the cairo context, and overlays face rectangles
     * with names and liveness status.
     * @param cr Cairo drawing context
     * @param widget The GTK drawing area widget
     */
    void update_image(cairo_t* cr, GtkWidget* widget) {
        {
            std::lock_guard<std::mutex> lock(_image_mutex);
            
            if (_image < 0)
                FSDK_FreeImage(_image);

            FSDK_GrabFrame(_camera, &_image);
            FSDK_MirrorImage(_image, true);
            FSDK_SaveImageToBuffer(_image, _data, FSDK_IMAGE_COLOR_24BIT);
        }

        gdk_cairo_set_source_pixbuf(cr,
            gdk_pixbuf_new_from_data(_data, GDK_COLORSPACE_RGB, false, 8, _width, _height, 3 * _width, nullptr, nullptr),
            0, 0);

        cairo_paint(cr);
        cairo_select_font_face(cr, "monospace", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
        cairo_set_font_size(cr, 14);

        bool selected = false;
        {
            std::lock_guard<std::mutex> lock(_faces_mutex);
            for (long long i = 0; i < _count; ++i) {
                const auto& pos = _faces[i].position;
                if (!_faces[i].isLive) {
                    cairo_set_source_rgb(cr, 1, 0, 0);
                } else {
                    if (!selected && contains(pos, _mouse_pos.first, _mouse_pos.second)) {
                        selected = true;
                        cairo_set_source_rgb(cr, 0, 0, 1);
                    } else {
                        cairo_set_source_rgb(cr, 0, 1, 0);
                    }
                }
                const auto w = (double)pos.w;
                const auto h = pos.w * 1.2;
                cairo_rectangle(cr, pos.xc - w / 2, pos.yc - h / 2, w, h);
                cairo_move_to(cr, pos.xc - w / 2, pos.yc + h / 2 + 14);
                cairo_show_text(cr, _faces[i].name);

                if (!_faces[i].status.empty()) {
                    cairo_move_to(cr, pos.xc - w / 2, pos.yc + h / 2 + 28);
                    cairo_show_text(cr, _faces[i].status.c_str());
                }
                cairo_stroke(cr);
            }
        }
    }

    /**
     * Handles mouse move events to track cursor position.
     * Updates the stored mouse position for face selection highlighting.
     * @param event GDK motion event containing mouse coordinates
     * @param widget The widget that received the event
     * @return True to stop event propagation
     */
    bool mouse_move(GdkEvent* event, GtkWidget* widget) {
        _mouse_pos.first =  ((GdkEventMotion*)event)->x;
        _mouse_pos.second = ((GdkEventMotion*)event)->y;
        return true;
    }

    /**
     * Handles mouse click events for face selection and naming.
     * When a live face is clicked, opens a dialog to enter/edit the person's name.
     * @param event GDK button event containing click coordinates
     * @param widget The widget that received the event
     * @return True to stop event propagation
     */
    bool mouse_click(GdkEvent* event, GtkWidget* widget) 
    {
        const auto x = ((GdkEventButton*)event)->x;
        const auto y = ((GdkEventButton*)event)->y;
        {
            std::lock_guard<std::mutex> lock(_faces_mutex);
            for (long long i = 0; i < _count; ++i) {
                if (!_faces[i].isLive) {
                    continue;
                }
                if (contains(_faces[i].position, x, y)) {
                    const auto dialog = gtk_message_dialog_new(GTK_WINDOW(_window),
                            static_cast<GtkDialogFlags>(GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL),
                            GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL, "Please enter your name");
                    const auto area = gtk_message_dialog_get_message_area((GtkMessageDialog*)dialog);
                    const auto entry = gtk_entry_new();
                    gtk_entry_set_text((GtkEntry*)entry, _faces[i].name);
                    gtk_container_add(GTK_CONTAINER(area), entry);
                    g_signal_connect(dialog, "response", G_CALLBACK(edit_name_response),
                            new response_signal_data_t(entry, _tracker, _faces[i].id));
                    gtk_widget_show_all(dialog);
                }
            }
        }
        return true;
    }

    /**
     * Callback for the name edit dialog response.
     * Saves the entered name to the tracker or purges the ID if name is empty.
     * @param widget The dialog widget
     * @param response The dialog response (OK or CANCEL)
     * @param data Tuple containing entry widget, tracker handle, and face ID
     */
    static void edit_name_response(GtkWidget* widget, int response, response_signal_data_t* data) 
    {
        const auto& entry = std::get<0>(*data);
        const auto& tracker = std::get<1>(*data);
        const auto& id = std::get<2>(*data);

        if (response == GTK_RESPONSE_OK && FSDK_LockID(tracker, id) == FSDKE_OK) {
            const auto name = gtk_entry_get_text((GtkEntry*)entry);
            if (name[0])
                FSDK_SetName(tracker, id, name);
            else
                FSDK_PurgeID(tracker, id);

            FSDK_UnlockID(tracker, id);
        }
        delete data;
        gtk_widget_destroy(widget);
    }

};

/**
 * Main application class that manages the GTK application lifecycle.
 */
class application {

public:

    // Default constructor
    application() = default;
    
    // Delete copy constructor
    application(const application&) = delete;
    
    // Delete move constructor
    application(application&&) = delete;
    
    // Delete copy assignment operator
    application& operator=(const application&) = delete;
    
    // Delete move assignment operator
    application& operator=(application&&) = delete;

    /**
     * Runs the GTK application main loop.
     * Initializes the application, sets up callbacks, and processes events.
     * @param argc Argument count from main
     * @param argv Argument values from main
     * @return Application exit status code
     */
    int run(int argc, char* argv[]) {
        _app = gtk_application_new("org.luxand.live_recognition", G_APPLICATION_DEFAULT_FLAGS);
        g_signal_connect_swapped(_app, "activate", cast_to_void_func(&application::activate), this);
        int status = g_application_run(G_APPLICATION(_app), argc, argv);
        delete _camera_window;
        g_object_unref(_app);
        return status;
    }

private:

    GtkApplication* _app;          // GTK application handle
    camera_window* _camera_window; // Camera window instance

    /**
     * Callback invoked when the GTK application is activated.
     * Creates the camera window instance.
     * @param app The GTK application (unused parameter)
     */
    void activate(GtkApplication*) {
        _camera_window = new camera_window(_app);
    }

};

/**
 * Main entry point of the application.
 * Initializes the FaceSDK library, creates and runs the application,
 * then cleans up resources before exiting.
 * @param argc Number of command-line arguments
 * @param argv Array of command-line argument strings
 * @return Exit status code (0 for success, -1 for activation error, or application result)
 */
int main(int argc, char* argv[]) 
{
    int res = FSDK_ActivateLibrary("INSERT THE LICENSE KEY HERE");
	if (res != FSDKE_OK) {
		std::cout << "Error activating FaceSDK" << std::endl;
		return -1;
	}

    FSDK_Initialize(nullptr);

    application app{};
    const auto result = app.run(argc, argv);

    FSDK_Finalize();

    return result;
}
