#include "odinmedia.h"
#include "odinroom.h"
#include "utilities.h"
#include <iostream>

using namespace std;

Napi::FunctionReference *OdinMediaWrapper::constructor;

/**
 * Constructs an OdinMediaWrapper for encoding and transmitting audio.
 * This maps to odin_encoder_create() in the core SDK.
 * 
 * @param info[0] OdinRoom instance - parent room for transmission
 * @param info[1] Sample rate (number) - typically 48000
 * @param info[2] Number of channels (number) - 1 for mono, 2 for stereo
 */
OdinMediaWrapper::OdinMediaWrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap<OdinMediaWrapper>(info) {
    Napi::Env env = info.Env();

    if (info.Length() < 3 || !info[0].IsObject() || !info[1].IsNumber() || !info[2].IsNumber()) {
        Napi::TypeError::New(env, "Provide an OdinRoom instance, sample rate and number of channels").ThrowAsJavaScriptException();
        return;
    }

    _roomWrapper = Napi::ObjectWrap<OdinRoomWrapper>::Unwrap(info[0].As<Napi::Object>());
    _sampleRate = info[1].As<Napi::Number>().Uint32Value();
    _numChannels = info[2].As<Napi::Number>().Int32Value();
    _mediaId = 0;  // Will be set via setMediaId() after Joined event
    _closed = false;
    _encoder = nullptr;

    // Create the ODIN encoder with the specified sample rate and stereo mode
    OdinError rc = odin_encoder_create(_sampleRate, _numChannels == 2, &_encoder);
    if (odin_is_error(rc)) {
       OdinUtilities::ThrowNapiException(env, rc, "Failed to create encoder");
    }
}

/**
 * Releases resources when the JavaScript object is garbage collected.
 * Uses mutex to prevent race conditions with Close().
 */
void OdinMediaWrapper::Finalize(Napi::Env env) {
    std::lock_guard<std::mutex> lock(_closeMutex);
    
    _closed = true;

    if (_encoder) {
        odin_encoder_free(_encoder);
        _encoder = nullptr;
    }
    
    _roomWrapper = nullptr;
}

/**
 * Initializes the OdinMedia class and registers it with the N-API module.
 * Exposes: sendAudioData, setMediaId, close, and id property.
 * 
 * Note: start() and stop() were removed as they served no purpose -
 * they set a _running flag that was never checked. This simplifies
 * the API to match the core ODIN SDK pattern.
 */
Napi::Object OdinMediaWrapper::Init(Napi::Env env, Napi::Object exports) {
    Napi::Function func = DefineClass(env, "OdinMedia", {
            InstanceMethod<&OdinMediaWrapper::Close>("close", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
            InstanceMethod<&OdinMediaWrapper::SendData>("sendAudioData", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
            InstanceMethod<&OdinMediaWrapper::SetMediaId>("setMediaId", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
            InstanceAccessor("id", &OdinMediaWrapper::MediaId, nullptr, static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
            StaticMethod<&OdinMediaWrapper::CreateNewItem>("CreateNewItem", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
    });

    constructor = new Napi::FunctionReference();
    *constructor = Napi::Persistent(func);
    exports.Set("OdinMedia", func);

    return exports;
}

/**
 * Closes the media stream and releases resources.
 * This maps to odin_encoder_free() in the core SDK.
 * 
 * Thread Safety:
 * Sets _closed to true FIRST (atomically), then acquires the mutex to free resources.
 * This ensures that other methods see _closed=true immediately and exit early.
 */
void OdinMediaWrapper::Close(const Napi::CallbackInfo &info) {
    // Set closed flag first - this is atomic and immediately visible to other methods.
    bool wasClosed = _closed.exchange(true);
    if (wasClosed) {
        return;  // Already closed
    }
    
    // Acquire mutex to safely free resources
    std::lock_guard<std::mutex> lock(_closeMutex);
    
    if (_encoder) {
        odin_encoder_free(_encoder);
        _encoder = nullptr;
    }
    
    _roomWrapper = nullptr;
}

Napi::Value OdinMediaWrapper::CreateNewItem(const Napi::CallbackInfo &info) {
    return constructor->New({info[0]});
}

Napi::Object OdinMediaWrapper::NewInstance(const std::vector<napi_value>& args) {
    return constructor->New(args);
}

/**
 * Sets the server-assigned media ID.
 * This must be called after receiving the Joined event which provides
 * the list of available media IDs for this peer.
 * 
 * @param info[0] Media ID (number) - from Joined event's mediaIds array
 */
void OdinMediaWrapper::SetMediaId(const Napi::CallbackInfo &info) {
    if (_closed) return;
    
    Napi::Env env = info.Env();
    
    if (info.Length() < 1 || !info[0].IsNumber()) {
        Napi::TypeError::New(env, "Media ID (number) required").ThrowAsJavaScriptException();
        return;
    }
    
    _mediaId = static_cast<uint16_t>(info[0].As<Napi::Number>().Uint32Value());
}

/**
 * Sends audio samples for transmission.
 * This maps to odin_encoder_push() + odin_encoder_pop() + odin_room_send_datagram() in the core SDK.
 * 
 * Pushes samples to the encoder, then immediately pops and sends all available datagrams.
 * If mediaId is 0 or the stream is closed, this is a no-op (matches core SDK behavior).
 *
 * @param info[0] Float32Array of interleaved audio samples in range [-1, 1]
 */
void OdinMediaWrapper::SendData(const Napi::CallbackInfo &info) {
    // Early exit if closed - prevents use-after-free
    if (_closed) return;
    
    Napi::Env env = info.Env();

    if (info.Length() < 1 || !info[0].IsTypedArray()) {
        Napi::TypeError::New(env, "Float32Array required as first parameter").ThrowAsJavaScriptException();
        return;
    }

    // Check resources are valid
    if (!_encoder || !_roomWrapper || !_roomWrapper->GetRoom()) {
        return;
    }
    
    // mediaId must be set before sending (will produce no output if 0)
    if (_mediaId == 0) {
        return;
    }

    Napi::Float32Array array = info[0].As<Napi::Float32Array>();
    const float* samples = array.Data();
    uint32_t sampleCount = array.ElementLength();

    // Push samples to encoder
    OdinError err = odin_encoder_push(_encoder, samples, sampleCount);
    if (odin_is_error(err)) {
        std::cerr << "odin_encoder_push failed: " << odin_error_get_last_error() << std::endl;
        return;
    }

    // Pop and send all available datagrams
    uint8_t datagram[2048];
    for (;;) {
        if (_closed) return;  // Check again in loop
        
        uint32_t datagram_length = sizeof(datagram);

        switch (odin_encoder_pop(_encoder, &_mediaId, 1, datagram, &datagram_length)) {
        case ODIN_ERROR_SUCCESS:
            if (_roomWrapper && _roomWrapper->GetRoom()) {
                odin_room_send_datagram(_roomWrapper->GetRoom(), datagram, datagram_length);
            }
            break;
        case ODIN_ERROR_NO_DATA:
            return;
        default:
            std::cerr << "odin_encoder_pop failed: " << odin_error_get_last_error() << std::endl;
            return;
        }
    }
}

/**
 * Returns the media ID for this stream.
 * @returns The server-assigned media ID, or 0 if not yet set.
 */
Napi::Value OdinMediaWrapper::MediaId(const Napi::CallbackInfo &info) {
    return Napi::Number::New(info.Env(), _mediaId); 
}