#include "capture.h"
#include <napi.h>
#include <errno.h>
#include <uv.h>

#include <memory>
#include <sstream>
#include <string>
#include <vector>

namespace {

struct CallbackData {
    Napi::Reference<Napi::Object> thisObj;
    Napi::Reference<Napi::Function> callback;
};

static const char* control_type_names[] = {
    "invalid",
    "int",
    "bool",
    "menu",
    "int64",
    "class",
    "string",
    "bitmask",
    "int_menu",
};

// Forward declarations
static Napi::Object cameraFormats(const camera_t* camera, const Napi::Env& env);
static Napi::Object cameraControls(const camera_t* camera, const Napi::Env& env);
static camera_format_t convertCFormat(const Napi::Object& format);
static Napi::Object convertFormat(const camera_format_t* cformat, const Napi::Env& env);

class Camera : public Napi::ObjectWrap<Camera> {
public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports);
    Camera(const Napi::CallbackInfo& info);
    ~Camera();

private:
    Napi::Value Start(const Napi::CallbackInfo& info);
    Napi::Value Stop(const Napi::CallbackInfo& info);
    Napi::Value Capture(const Napi::CallbackInfo& info);
    Napi::Value FrameRaw(const Napi::CallbackInfo& info);
    Napi::Value FrameYUYVToRGB(const Napi::CallbackInfo& info);
    Napi::Value ConfigGet(const Napi::CallbackInfo& info);
    Napi::Value ConfigSet(const Napi::CallbackInfo& info);
    Napi::Value ControlGet(const Napi::CallbackInfo& info);
    Napi::Value ControlSet(const Napi::CallbackInfo& info);

    static void StopCB(uv_poll_t* handle, int status, int events);
    static void CaptureCB(uv_poll_t* handle, int status, int events);
    
    static void WatchCB(uv_poll_t* handle, void (*callbackCall)(CallbackData* data));
    void Watch(const Napi::CallbackInfo& info, uv_poll_cb cb);
    
    camera_t* camera;
};

// Error handling
struct LogContext {
    std::string msg;
};

static void logRecord(camera_log_t type, const char* msg, void* pointer) {
    std::stringstream ss;
    switch (type) {
    case CAMERA_ERROR:
        ss << "CAMERA ERROR [" << msg << "] " << errno << " " << strerror(errno);
        break;
    case CAMERA_FAIL:
        ss << "CAMERA FAIL [" << msg << "]";
        break;
    case CAMERA_INFO:
        ss << "CAMERA INFO [" << msg << "]";
        break;
    }
    static_cast<LogContext*>(pointer)->msg = ss.str();
}

static void camera_log(const camera_t* camera, void (*callback)(camera_log_t, const char*, void*), void* pointer) {
    if (camera && camera->context.pointer) {
        auto ctx = static_cast<LogContext*>(camera->context.pointer);
        callback(CAMERA_ERROR, ctx->msg.c_str(), pointer);
    }
}

Napi::Value cameraError(const Napi::Env& env, const camera_t* camera) {
    LogContext context;
    camera_log(camera, logRecord, &context);
    return Napi::Error::New(env, context.msg).Value();
}

// Helper functions
static Napi::Object cameraControls(const camera_t* camera, const Napi::Env& env) {
    auto ccontrols = camera_controls_new(camera);
    auto controls = Napi::Array::New(env, ccontrols->length);
    
    for (auto i = std::size_t{0}; i < ccontrols->length; ++i) {
        const auto ccontrol = &ccontrols->head[i];
        auto control = Napi::Object::New(env);
        const auto name = Napi::String::New(env, reinterpret_cast<char*>(ccontrol->name));
        controls[uint32_t(i)] = control;
        control.Set(name, control);
        control.Set("id", Napi::Number::New(env, ccontrol->id));
        control.Set("name", name);
        control.Set("type", Napi::String::New(env, control_type_names[ccontrol->type]));
        control.Set("min", Napi::Number::New(env, ccontrol->min));
        control.Set("max", Napi::Number::New(env, ccontrol->max));
        control.Set("step", Napi::Number::New(env, ccontrol->step));
        control.Set("default", Napi::Number::New(env, ccontrol->default_value));
        
        auto flags = Napi::Object::New(env);
        control.Set("flags", flags);
        flags.Set("disabled", Napi::Boolean::New(env, ccontrol->flags.disabled));
        flags.Set("grabbed", Napi::Boolean::New(env, ccontrol->flags.grabbed));
        flags.Set("readOnly", Napi::Boolean::New(env, ccontrol->flags.read_only));
        flags.Set("update", Napi::Boolean::New(env, ccontrol->flags.update));
        flags.Set("inactive", Napi::Boolean::New(env, ccontrol->flags.inactive));
        flags.Set("slider", Napi::Boolean::New(env, ccontrol->flags.slider));
        flags.Set("writeOnly", Napi::Boolean::New(env, ccontrol->flags.write_only));
        flags.Set("volatile", Napi::Boolean::New(env, ccontrol->flags.volatile_value));
        
        auto menu = Napi::Array::New(env, ccontrol->menus.length);
        control.Set("menu", menu);
        switch (ccontrol->type) {
        case CAMERA_CTRL_MENU:
            for (auto j = std::size_t{0}; j < ccontrol->menus.length; ++j) {
                menu[uint32_t(j)] = Napi::String::New(env, reinterpret_cast<char*>(ccontrol->menus.head[j].name));
            }
            break;
#ifndef CAMERA_OLD_VIDEODEV2_H
        case CAMERA_CTRL_INTEGER_MENU:
            for (auto j = std::size_t{0}; j < ccontrol->menus.length; ++j) {
                menu[uint32_t(j)] = Napi::Number::New(env, static_cast<std::int32_t>(ccontrol->menus.head[j].value));
            }
            break;
#endif
        default: break;
        }
    }
    camera_controls_delete(ccontrols);
    return controls;
}

static camera_format_t convertCFormat(const Napi::Object& format) {
    const auto pixformat = format.Get("format").As<Napi::Number>().Uint32Value();
    const auto width = format.Get("width").As<Napi::Number>().Uint32Value();
    const auto height = format.Get("height").As<Napi::Number>().Uint32Value();
    auto numerator = std::uint32_t{0};
    auto denominator = std::uint32_t{0};
    
    const auto finterval = format.Get("interval");
    if (finterval.IsObject()) {
        const auto interval = finterval.As<Napi::Object>();
        numerator = interval.Get("numerator").As<Napi::Number>().Uint32Value();
        denominator = interval.Get("denominator").As<Napi::Number>().Uint32Value();
    }
    
    return {pixformat, width, height, {numerator, denominator}};
}

static Napi::Object convertFormat(const camera_format_t* cformat, const Napi::Env& env) {
    char name[5];
    camera_format_name(cformat->format, name);
    auto format = Napi::Object::New(env);
    format.Set("formatName", Napi::String::New(env, name));
    format.Set("format", Napi::Number::New(env, cformat->format));
    format.Set("width", Napi::Number::New(env, cformat->width));
    format.Set("height", Napi::Number::New(env, cformat->height));
    
    auto interval = Napi::Object::New(env);
    format.Set("interval", interval);
    interval.Set("numerator", Napi::Number::New(env, cformat->interval.numerator));
    interval.Set("denominator", Napi::Number::New(env, cformat->interval.denominator));
    return format;
}

static Napi::Object cameraFormats(const camera_t* camera, const Napi::Env& env) {
    auto cformats = camera_formats_new(camera);
    auto formats = Napi::Array::New(env, cformats->length);
    for (auto i = std::size_t{0}; i < cformats->length; ++i) {
        auto cformat = &cformats->head[i];
        formats[uint32_t(i)] = convertFormat(cformat, env);
    }
    camera_formats_delete(cformats);
    return formats;
}

// Camera implementation
Camera::Camera(const Napi::CallbackInfo& info) 
    : Napi::ObjectWrap<Camera>(info), camera(nullptr) {
    auto env = info.Env();
    
    if (info.Length() < 1 || !info[0].IsString()) {
        Napi::TypeError::New(env, "Device path required").ThrowAsJavaScriptException();
        return;
    }
    
    std::string device = info[0].As<Napi::String>();
    camera = camera_open(device.c_str());
    if (!camera) {
        Napi::Error::New(env, "Camera not found").ThrowAsJavaScriptException();
        return;
    }
    
    camera->context.pointer = new LogContext;

    auto thisObj = info.This().As<Napi::Object>();
    thisObj.Set("device", info[0]);
    thisObj.Set("formats", cameraFormats(camera, env));
    thisObj.Set("controls", cameraControls(camera, env));
}

Camera::~Camera() {
    if (camera) {
        auto ctx = static_cast<LogContext*>(camera->context.pointer);
        camera_close(camera);
        delete ctx;
    }
}

void Camera::WatchCB(uv_poll_t* handle, void (*callbackCall)(CallbackData* data)) {
    auto data = static_cast<CallbackData*>(handle->data);
    uv_poll_stop(handle);
    uv_close(reinterpret_cast<uv_handle_t*>(handle), 
             [](uv_handle_t* handle) -> void {
                 delete handle;
             });
    callbackCall(data);
    data->thisObj.Reset();
    delete data;
}

void Camera::Watch(const Napi::CallbackInfo& info, uv_poll_cb cb) {
    auto env = info.Env();
    if (info.Length() < 1 || !info[0].IsFunction()) {
        Napi::TypeError::New(env, "Argument must be a function").ThrowAsJavaScriptException();
        return;
    }
    int fd = camera->fd;
    if (fd < 0) {
        cameraError(env, camera);
        return;
    }
    auto data = new CallbackData;
    data->thisObj = Napi::Reference<Napi::Object>::New(info.This().As<Napi::Object>(), 1);
    data->callback = Napi::Reference<Napi::Function>::New(info[0].As<Napi::Function>(), 1);
    auto handle = new uv_poll_t;
    handle->data = data;
    uv_poll_init(uv_default_loop(), handle, fd);
    uv_poll_start(handle, UV_READABLE, cb);
}

void Camera::StopCB(uv_poll_t* handle, int /*status*/, int /*events*/) {
    auto callCallback = [](CallbackData* data) -> void {
        Napi::HandleScope scope(data->thisObj.Env());
        auto thisObj = data->thisObj.Value();
        data->callback.Value().Call(thisObj, {});
    };
    WatchCB(handle, callCallback);
}

void Camera::CaptureCB(uv_poll_t* handle, int /*status*/, int /*events*/) {
    auto callCallback = [](CallbackData* data) -> void {
        Napi::HandleScope scope(data->thisObj.Env());
        auto thisObj = data->thisObj.Value();
        auto camera = Napi::ObjectWrap<Camera>::Unwrap(thisObj)->camera;
        auto captured = camera_capture(camera);
        data->callback.Value().Call(thisObj, {Napi::Boolean::New(data->thisObj.Env(), captured)});
    };
    WatchCB(handle, callCallback);
}

Napi::Value Camera::Start(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (!camera_start(camera)) {
        return cameraError(env, camera);
    }
    auto thisObj = info.This().As<Napi::Object>();
    thisObj.Set("width", Napi::Number::New(env, camera->width));
    thisObj.Set("height", Napi::Number::New(env, camera->height));
    return thisObj;
}

Napi::Value Camera::Stop(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (!camera_stop(camera)) {
        return cameraError(env, camera);
    }
    if (info.Length() < 1) {
        return env.Undefined();
    }
    Watch(info, StopCB);
    return env.Undefined();
}

Napi::Value Camera::Capture(const Napi::CallbackInfo& info) {
    Watch(info, CaptureCB);
    return info.Env().Undefined();
}

Napi::Value Camera::FrameRaw(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    const auto size = camera->head.length;
    auto data = new uint8_t[size];
    std::memcpy(data, camera->head.start, size);
    
    auto buffer = Napi::ArrayBuffer::New(
        env, 
        data, 
        size,
        [](Napi::Env /*env*/, void* data) {
            delete[] static_cast<uint8_t*>(data);
        }
    );
    
    auto array = Napi::Uint8Array::New(env, size, buffer, 0);
    return array;
}

Napi::Value Camera::FrameYUYVToRGB(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    const auto width = camera->width;
    const auto height = camera->height;
    const auto size = width * height * 3;
    
    uint8_t* rgb_data = yuyv2rgb(camera->head.start, width, height);
    
    auto buffer = Napi::ArrayBuffer::New(
        env, 
        rgb_data, 
        size,
        [](Napi::Env /*env*/, void* data) {
            delete[] static_cast<uint8_t*>(data);
        }
    );
    
    auto array = Napi::Uint8Array::New(env, size, buffer, 0);
    return array;
}

Napi::Value Camera::ConfigGet(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    camera_format_t cformat;
    if (!camera_config_get(camera, &cformat)) {
        return cameraError(env, camera);
    }
    return convertFormat(&cformat, env);
}

Napi::Value Camera::ConfigSet(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (info.Length() < 1 || !info[0].IsObject()) {
        Napi::TypeError::New(env, "Config object required").ThrowAsJavaScriptException();
        return env.Undefined();
    }
    
    const auto cformat = convertCFormat(info[0].As<Napi::Object>());
    if (!camera_config_set(camera, &cformat)) {
        return cameraError(env, camera);
    }
    
    auto thisObj = info.This().As<Napi::Object>();
    thisObj.Set("width", Napi::Number::New(env, camera->width));
    thisObj.Set("height", Napi::Number::New(env, camera->height));
    return thisObj;
}

Napi::Value Camera::ControlGet(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (info.Length() < 1) {
        Napi::TypeError::New(env, "Control ID required").ThrowAsJavaScriptException();
        return env.Undefined();
    }
    
    const auto id = info[0].As<Napi::Number>().Uint32Value();
    auto value = std::int32_t{0};
    if (!camera_control_get(camera, id, &value)) {
        return cameraError(env, camera);
    }
    return Napi::Number::New(env, value);
}

Napi::Value Camera::ControlSet(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    if (info.Length() < 2) {
        Napi::TypeError::New(env, "Control ID and value required").ThrowAsJavaScriptException();
        return env.Undefined();
    }
    
    const auto id = info[0].As<Napi::Number>().Uint32Value();
    const auto value = info[1].As<Napi::Number>().Int32Value();
    if (!camera_control_set(camera, id, value)) {
        return cameraError(env, camera);
    }
    return info.This();
}

Napi::Object Camera::Init(Napi::Env env, Napi::Object exports) {
    Napi::Function func = DefineClass(env, "Camera", {
        InstanceMethod("start", &Camera::Start),
        InstanceMethod("stop", &Camera::Stop),
        InstanceMethod("capture", &Camera::Capture),
        InstanceMethod("frameRaw", &Camera::FrameRaw),
        InstanceMethod("frameYUYVToRGB", &Camera::FrameYUYVToRGB),
        InstanceMethod("configGet", &Camera::ConfigGet),
        InstanceMethod("configSet", &Camera::ConfigSet),
        InstanceMethod("controlGet", &Camera::ControlGet),
        InstanceMethod("controlSet", &Camera::ControlSet)
    });

    Napi::FunctionReference* constructor = new Napi::FunctionReference();
    *constructor = Napi::Persistent(func);
    env.SetInstanceData(constructor);

    exports.Set("Camera", func);
    return exports;
}

} // namespace

Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
    return Camera::Init(env, exports);
}

NODE_API_MODULE(v4l2camera, InitAll)
