#define LIBFEEDBACK_USE_UNSTABLE_API 1

#include <droidmedia/droidmediacamera.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <gtkmm.h>
#include <glibmm.h>
#include <memory>
#include <vector>
#include <libfeedback.h>
#include <iostream>
#include <thread>
#include <chrono>
#include <ctime>
#include <sstream>
#include <iomanip>
#include <unordered_map>

static inline uint8_t clamp(int v) {
    return v < 0 ? 0 : (v > 255 ? 255 : v);
}

inline void set_expand_in_direction(Gtk::Widget &widget, Gtk::Orientation orientation) {
    if(orientation == Gtk::Orientation::HORIZONTAL) {
        widget.set_vexpand(false);
        widget.set_hexpand(true);
    } else {
        widget.set_vexpand(true);
        widget.set_hexpand(false);
    }
}

std::string write_parameters(std::unordered_map<std::string, std::string> const parameters) {
    std::stringstream str;
    for(auto const &param: parameters) {
        str << param.first << '=' << param.second << ';';
    }
    return str.str();
}

std::map<std::string, int> const ORIENTATIONS = {
    {"", 0},
    {"normal", 0},
    {"left-up", 90},
    {"bottom-up", 180},
    {"right-up", 270}
};

std::vector<std::pair<int, int>> parse_size_list(std::string const &sizes) {
    std::vector<std::pair<int, int>> result;
    std::stringstream str(sizes);
    while(!str.eof()) {
        int w, h;
        str >> w; str.get(); str >> h; str.get();
        result.emplace_back(w, h);
    }
    return result;
}

class AndroCamera : public Glib::Object {
private:
    int camera_id;
    DroidMediaCameraCallbacks cbs{};
    DroidMediaBufferQueueCallbacks buffer_cbs{};
    DroidMediaBufferQueue *bq = nullptr;

    static void buffers_released_cb(void *self) {
        fprintf(stderr, "buffers_released_cb\n");
    }

    static bool frame_available_cb(void *self, DroidMediaBuffer *buffer) {
        auto cam = static_cast<AndroCamera*>(self);

        DroidMediaBufferInfo info{};
        droid_media_buffer_get_info(buffer, &info);

        DroidMediaBufferYCbCr ycbcr{};
        if(!droid_media_buffer_lock_ycbcr(buffer, DROID_MEDIA_BUFFER_LOCK_READ, &ycbcr)) {
            droid_media_buffer_release(buffer, nullptr, nullptr);
            return true;
        }
        auto rgb = std::make_shared<std::vector<uint8_t>>(info.width * info.height * 4);
        uint8_t *dst = rgb->data();
        for(int y = 0; y < info.height; ++y) {
            uint8_t *Yrow  = static_cast<uint8_t*>(ycbcr.y) + y * ycbcr.ystride;
            uint8_t *VUrow = static_cast<uint8_t*>(ycbcr.cb) + (y / 2) * ycbcr.cstride;
            uint8_t *UVrow = static_cast<uint8_t*>(ycbcr.cr) + (y / 2) * ycbcr.cstride;

            for(int x = 0; x < info.width; ++x) {
                int yy = Yrow[x];

                int cr = UVrow[(x / 2) * ycbcr.chroma_step] - 128;
                int cb = VUrow[(x / 2) * ycbcr.chroma_step] - 128;

                int r = yy + ((91881 * cr) >> 16);
                int g = yy - ((22554 * cb + 46802 * cr) >> 16);
                int b = yy + ((116130 * cb) >> 16);

                *dst++ = clamp(b);
                *dst++ = clamp(g);
                *dst++ = clamp(r);
                *dst++ = 0xFF;
            }
        }

        droid_media_buffer_unlock(buffer);
        droid_media_buffer_release(buffer, nullptr, nullptr);

        // Send to GTK main thread safely
        Glib::signal_idle().connect_once([cam, rgb, w = info.width, h = info.height] {
            cam->signal_frame_available.emit(rgb, w, h);
        });

        return true;
    }

    static bool buffer_created_cb(void *self, DroidMediaBuffer *) {
        return true;
    }

    static void shutter_cb(void *self) {
        fprintf(stderr, "shutter_cb\n");
    }

    static void compressed_image_cb(void *self, DroidMediaData *mem) {
        if(!mem || !mem->data || mem->size == 0) {
            fprintf(stderr, "compressed_image_cb: empty image\n");
            return;
        }
        if(auto self_casted = static_cast<AndroCamera*>(self)) {
            self_casted->signal_compressed_image.emit(mem);
        }
    }

public:
    DroidMediaCamera *camera = nullptr;
    sigc::signal<void(DroidMediaData*)> signal_compressed_image;
    sigc::signal<void(std::shared_ptr<std::vector<uint8_t>>, int, int)> signal_frame_available;
    std::unordered_map<std::string, std::string> parameters;
    DroidMediaCameraInfo physical_info;
    std::vector<std::pair<int, int>> picture_sizes, preview_sizes, video_sizes;

    explicit AndroCamera(int camera_id) : Glib::Object(), camera_id(camera_id) {}

    void connect() {
        // Provides facing and orientation
        droid_media_camera_get_info(&physical_info, camera_id);

        camera = droid_media_camera_connect(camera_id);

        cbs.shutter_cb = shutter_cb;
        cbs.compressed_image_cb = compressed_image_cb;
        droid_media_camera_set_callbacks(camera, &cbs, this);

        buffer_cbs.buffers_released = buffers_released_cb;
        buffer_cbs.frame_available = frame_available_cb;
        buffer_cbs.buffer_created = buffer_created_cb;

        bq = droid_media_camera_get_buffer_queue(camera);
        droid_media_buffer_queue_set_callbacks(bq, &buffer_cbs, this);
        
        std::stringstream param;
        param << droid_media_camera_get_parameters(camera);
        while(!param.eof()) {
            std::string key, value;
            std::getline(param, key, '=');
            std::getline(param, value, ';');
            parameters[key] = value;
        }
        
        picture_sizes = parse_size_list(parameters.at("picture-size-values"));
        video_sizes = parse_size_list(parameters.at("video-size-values"));
        preview_sizes = parse_size_list(parameters.at("preview-size-values"));
    }

    void disconnect() {
        droid_media_camera_disconnect(camera);
        camera = nullptr;
        cbs = {};
        buffer_cbs = {};
        physical_info = {};
    }
};

class PanoramaCamera : public Gtk::Application {
    Gtk::Window *window = nullptr;
    Gtk::Box *box = nullptr, *format_box = nullptr, *format_resolution_control = nullptr, *control_row = nullptr;
    Gtk::Scale *format_scale = nullptr;
    Gtk::Revealer *format_bar = nullptr;
    Gtk::DrawingArea *drawing = nullptr;
    Gtk::Button *capture_button = nullptr, *format_down_button = nullptr, *format_up_button = nullptr;
    Gtk::Label *format_label = nullptr;

    Glib::RefPtr<Gtk::Adjustment> format_adjustment;

    std::unique_ptr<AndroCamera> cam;
    Cairo::RefPtr<Cairo::ImageSurface> surface;
    Glib::RefPtr<Gio::DBus::Connection> dbus;
    Glib::RefPtr<Gio::DBus::Proxy> accelerometer;

    Glib::RefPtr<Gtk::CssProvider> css;
    Glib::RefPtr<Gdk::Surface> window_surface;

    sigc::connection compressed_image_connection, frame_available_connection, accelerometer_connection;

    std::unordered_map<std::string, std::string> parameters;

    int device_rotation_degrees = 0;

    void on_compressed_image(DroidMediaData *mem) {
        std::vector<uint8_t> image_copy(static_cast<uint8_t*>(mem->data), static_cast<uint8_t*>(mem->data) + mem->size);
        std::thread([image_copy = std::move(image_copy)]() mutable {
            auto now = std::chrono::system_clock::now();
            auto now_time_t = std::chrono::system_clock::to_time_t(now);
            std::tm local_time = *std::localtime(&now_time_t);
            auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;

            std::ostringstream filename;
            filename.imbue(std::locale::classic());
            filename << (local_time.tm_year + 1900) << '-'
                     << std::setw(2) << std::setfill('0') << (local_time.tm_mon + 1) << '-'
                     << std::setw(2) << std::setfill('0') << local_time.tm_mday << '_'
                     << std::setw(2) << std::setfill('0') << local_time.tm_hour << '-'
                     << std::setw(2) << std::setfill('0') << local_time.tm_min << '-'
                     << std::setw(2) << std::setfill('0') << local_time.tm_sec << '.'
                     << std::setw(3) << std::setfill('0') << ms.count()
                     << ".jpeg";
            FILE *f = fopen(filename.str().c_str(), "wb");
            if(!f) {
                perror("fopen");
                return;
            }
            fwrite(image_copy.data(), 1, image_copy.size(), f);
            fclose(f);
            fprintf(stderr, "%s written: %zu bytes\n", filename.str().c_str(), image_copy.size());
        }).detach();
    }

    void on_frame_available(std::shared_ptr<std::vector<uint8_t>> pixels, int width, int height) {
        // TODO: fix repeated allocations
        if(!(width && height && pixels)) {
            return;
        }
        int actual_orientation = (cam->physical_info.orientation + device_rotation_degrees) % 360;
        if(actual_orientation == 0) {
            surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, width, height);

            uint8_t *dst = surface->get_data();
            const uint8_t *src = pixels->data();
            int stride = surface->get_stride();

            for(int y = 0; y < height; ++y) {
                memcpy(dst + y * stride, src + y * width * 4, width * 4);
            }

            surface->mark_dirty();
            drawing->queue_draw();
        } else if(actual_orientation == 180) {
            surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, width, height);

            uint8_t *dst = surface->get_data();
            const uint8_t *src = pixels->data();
            int stride = surface->get_stride();

            for(int y = 0; y < height; ++y) {
                for(int x = 0; x < width; ++x) {
                    const uint8_t *src_px = src + ((height - 1 - y) * width + (width - 1 - x)) * 4;
                    uint8_t *dst_px = dst + y * stride + x * 4;
                    memcpy(dst_px, src_px, 4);
                }
            }

            surface->mark_dirty();
            drawing->queue_draw();
        } else if(actual_orientation == 90) {
            surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, height, width);

            uint8_t *dst = surface->get_data();
            const uint8_t *src = pixels->data();
            int stride = surface->get_stride();

            for(int y = 0; y < width; ++y) {
                for(int x = 0; x < height; ++x) {
                    const uint8_t *src_px = src + ((height - 1 - x) * width + y) * 4;
                    uint8_t *dst_px = dst + y * stride + x * 4;
                    memcpy(dst_px, src_px, 4);
                }
            }

            surface->mark_dirty();
            drawing->queue_draw();
        } else if(actual_orientation == 270) {
            surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, height, width);

            uint8_t *dst = surface->get_data();
            const uint8_t *src = pixels->data();
            int stride = surface->get_stride();

            for(int y = 0; y < width; ++y) {
                for(int x = 0; x < height; ++x) {
                    const uint8_t *src_px = src + ((height - 1 - x) * width + y) * 4;
                    uint8_t *dst_px = dst + (width - y - 1) * stride + (height - x - 1) * 4;
                    memcpy(dst_px, src_px, 4);
                }
            }

            surface->mark_dirty();
            drawing->queue_draw();
        }
    }

public:
    PanoramaCamera() : Gtk::Application("com.roundabout_host.roundabout.Camera", Gio::Application::Flags::NONE) {}

    static Glib::RefPtr<PanoramaCamera> create() {
        return Glib::make_refptr_for_instance<PanoramaCamera>(new PanoramaCamera());
    }

    void on_startup() override {
        // TODO: i18n
        Gtk::Application::on_startup();
        dbus = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM);
        droid_media_init();
        lfb_init("com.roundabout_host.roundabout.Camera", nullptr);
        css = Gtk::CssProvider::create();
        css->load_from_string(
            ".panorama-camera-window { "
            "background-color: #000000; "
            "}"
        );
        Gtk::StyleContext::add_provider_for_display(Gdk::Display::get_default(), css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);

        int count = droid_media_camera_get_number_of_cameras();
        if(count < 1) {
            fprintf(stderr, "No cameras available!\n");
            return;
        }

        cam = std::make_unique<AndroCamera>(0);
        cam->connect();
        
        format_adjustment = Gtk::Adjustment::create(0, 0, cam->picture_sizes.size() - 1);
        format_adjustment->signal_value_changed().connect([this]() {
            int value = format_adjustment->get_value(), w = cam->picture_sizes.at(cam->picture_sizes.size() - 1 - value).first, h = cam->picture_sizes.at(cam->picture_sizes.size() - 1 - value).second;
            droid_media_camera_stop_preview(cam->camera);
            parameters["picture-size"] = std::to_string(w) + "x" + std::to_string(h);

            std::string new_params = write_parameters(parameters);
            droid_media_camera_set_parameters(cam->camera, new_params.c_str());
            droid_media_camera_start_preview(cam->camera);
            droid_media_camera_set_parameters(cam->camera, new_params.c_str());
            if(format_label) {
                format_label->set_text(std::to_string(w) + "×" + std::to_string(h));
            }
            std::cerr << w << 'x' << h << '\n';
        });
        
        std::cout << droid_media_camera_get_parameters(cam->camera) << '\n';

        parameters["picture-size"] = "1920x1080";
        parameters["preview-size"] = "1024x768";
        parameters["video-size"] = "176x144";
        parameters["sensitivity"] = "100";
        parameters["exposure-time"] = "83200";
        parameters["flash-mode"] = "off";

        droid_media_camera_set_parameters(
            cam->camera,
            write_parameters(parameters).c_str()
        );
        
        compressed_image_connection = cam->signal_compressed_image.connect(sigc::mem_fun(*this, &PanoramaCamera::on_compressed_image));
        frame_available_connection = cam->signal_frame_available.connect(sigc::mem_fun(*this, &PanoramaCamera::on_frame_available));

        if(!cam->camera) {
            fprintf(stderr, "Failed to connect camera!\n");
            return;
        }

        if(!droid_media_camera_start_preview(cam->camera)) {
            fprintf(stderr, "start_preview failed\n");
            cam->disconnect();
            return;
        }

        droid_media_camera_set_parameters(
            cam->camera,
            write_parameters(parameters).c_str()
        );

        fprintf(stderr, "start_preview OK\n");
    }

    void set_gui_orientation(Gtk::Orientation orientation) {
        box->set_orientation(orientation);
        auto reverse_orientation = orientation == Gtk::Orientation::HORIZONTAL? Gtk::Orientation::VERTICAL : Gtk::Orientation::HORIZONTAL;

        control_row->set_orientation(reverse_orientation);
        format_scale->set_orientation(reverse_orientation);
        format_resolution_control->set_orientation(reverse_orientation);

        set_expand_in_direction(*format_bar, reverse_orientation);
        set_expand_in_direction(*format_scale, reverse_orientation);

        if(orientation == Gtk::Orientation::HORIZONTAL) {
            format_scale->set_inverted(true);
            format_bar->set_transition_type(Gtk::RevealerTransitionType::SLIDE_LEFT);
            format_down_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-down-symbolic")));
            format_up_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-up-symbolic")));
            format_resolution_control->reorder_child_after(*format_down_button, *format_scale);
            format_resolution_control->reorder_child_at_start(*format_up_button);
        } else {
            format_scale->set_inverted(false);
            format_bar->set_transition_type(Gtk::RevealerTransitionType::SLIDE_UP);
            format_down_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
            format_up_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
            format_resolution_control->reorder_child_after(*format_up_button, *format_scale);
            format_resolution_control->reorder_child_at_start(*format_down_button);
        }
    }

    void set_window_size() {
        if(window_surface->get_width() > window_surface->get_height()) {
            set_gui_orientation(Gtk::Orientation::HORIZONTAL);
        } else {
            set_gui_orientation(Gtk::Orientation::VERTICAL);
        }
    }

    void on_orientation_changed(std::string const &orientation) {
        device_rotation_degrees = ORIENTATIONS.at(orientation);
        int actual_orientation = (cam->physical_info.orientation + device_rotation_degrees) % 360;
        std::cout << "rotation is " << actual_orientation << " degrees!\n";
        parameters["jpeg-orientation"] = std::to_string(actual_orientation);
        droid_media_camera_set_parameters(cam->camera, write_parameters(parameters).c_str());
    }

    void on_activate() override {
        Gtk::Application::on_activate();
        window = Gtk::make_managed<Gtk::Window>();
        auto settings = window->get_settings();
        settings->property_gtk_application_prefer_dark_theme().set_value(true);
        drawing = Gtk::make_managed<Gtk::DrawingArea>();

        drawing->set_draw_func([this](const Cairo::RefPtr<Cairo::Context>& cr, int w, int h) {
            if(surface) {
                double scale = std::min(double(w) / surface->get_width(),
                        double(h) / surface->get_height());
                double offset_x = (w - surface->get_width() * scale) / 2;
                double offset_y = (h - surface->get_height() * scale) / 2;
                cr->translate(offset_x, offset_y);
                cr->scale(scale, scale);
                cr->set_source(surface, 0, 0);
                cr->paint();
            }
        });

        window->add_css_class("panorama-camera-window");
        box = Gtk::make_managed<Gtk::Box>();
        box->append(*drawing);
        control_row = Gtk::make_managed<Gtk::Box>();
        
        format_bar = Gtk::make_managed<Gtk::Revealer>();
        auto format_button = Gtk::make_managed<Gtk::ToggleButton>();
        format_label = Gtk::make_managed<Gtk::Label>();
        format_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-x-generic-symbolic")));
        format_button->signal_clicked().connect([this, format_button]() {
            format_bar->set_reveal_child(format_button->get_active());
        });
        format_scale = Gtk::make_managed<Gtk::Scale>();
        format_scale->set_adjustment(format_adjustment);
        format_scale->set_round_digits(0);
        format_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
        format_resolution_control = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
        format_down_button = Gtk::make_managed<Gtk::Button>();
        format_down_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
        format_down_button->signal_clicked().connect([this]() {
            format_adjustment->set_value(std::max(format_adjustment->get_lower(), format_adjustment->get_value() - 1));
        });
        format_up_button = Gtk::make_managed<Gtk::Button>();
        format_up_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
        format_up_button->signal_clicked().connect([this]() {
            format_adjustment->set_value(std::min(format_adjustment->get_upper(), format_adjustment->get_value() + 1));
        });
        format_resolution_control->append(*format_down_button);
        format_resolution_control->append(*format_scale);
        format_resolution_control->append(*format_up_button);
        format_box->append(*format_label);
        format_box->append(*format_resolution_control);
        format_bar->set_child(*format_box);
        format_bar->set_hexpand(true);
        control_row->append(*format_button);
        box->append(*format_bar);

        format_adjustment->set_value(format_adjustment->get_upper());
        
        box->append(*control_row);
        capture_button = Gtk::make_managed<Gtk::Button>();
        capture_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("camera-photo-symbolic")));
        capture_button->signal_clicked().connect([this]() {
            LfbEvent *event = lfb_event_new("camera-shutter");
            lfb_event_trigger_feedback(event, NULL);
            g_object_unref(event);
            droid_media_camera_take_picture(cam->camera, 0);
        });
        box->append(*capture_button);
        drawing->set_hexpand(true);
        drawing->set_vexpand(true);
        window->set_child(*box);
        add_window(*window);
        window->present();
        window_surface = window->get_surface();
        window_surface->property_width().signal_changed().connect(sigc::mem_fun(*this, &PanoramaCamera::set_window_size));
        window_surface->property_height().signal_changed().connect(sigc::mem_fun(*this, &PanoramaCamera::set_window_size));
        
        for(auto e: cam->picture_sizes) {
            std::cout << e.first << "*" << e.second << '\n';
        }

        // To support screen rotation
        accelerometer = Gio::DBus::Proxy::create_sync(
            dbus,
            "net.hadess.SensorProxy",
            "/net/hadess/SensorProxy",
            "net.hadess.SensorProxy"
        );
        Glib::VariantBase value;
        accelerometer->get_cached_property(value, "AccelerometerOrientation");

        if(value) {
            auto orientation = value.get_dynamic<std::string>();
            on_orientation_changed(orientation);
        }
        accelerometer->call_sync("ClaimAccelerometer");
        accelerometer_connection = accelerometer->signal_properties_changed().connect([this](const Gio::DBus::Proxy::MapChangedProperties& changed, const std::vector<Glib::ustring>&) {
                auto it = changed.find("AccelerometerOrientation");
                if(it != changed.end()) {
                    auto orientation = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(it->second).get();
                    on_orientation_changed(orientation);
                }
            }
        );
    }

    void on_shutdown() override {
        Gtk::Application::on_shutdown();
        accelerometer->call_sync("ReleaseAccelerometer");
        if(accelerometer_connection) {
            accelerometer_connection.disconnect();
        }
        if(frame_available_connection) {
            frame_available_connection.disconnect();
        }
        if(compressed_image_connection) {
            compressed_image_connection.disconnect();
        }
        droid_media_camera_stop_preview(cam->camera);
        cam->disconnect();
    }
};

int main(int argc, char *argv[]) {
    auto app = PanoramaCamera::create();
    return app->run(argc, argv);
}

