#include <string.h>
#include "types.h"
#include "result.h"
#include "services/vi.h"
#include "display/binder.h"
#include "display/buffer_producer.h"
#include "display/native_window.h"
#include "nvidia/graphic_buffer.h"

#define NWINDOW_MAGIC 0x6E69574E // NWin

static void _nwindowUpdate(NWindow* nw, const BqBufferOutput* out)
{
    nw->default_width = out->width;
    nw->default_height = out->height;
    nw->consumer_running_behind = out->numPendingBuffers > 1;
}

static Result _nwindowConnect(NWindow* nw)
{
    BqBufferOutput bqoutput;
    Result rc = bqConnect(&nw->bq, NATIVE_WINDOW_API_CPU, nw->producer_controlled_by_app, &bqoutput);
    if (R_SUCCEEDED(rc)) {
        nw->is_connected = true;
        _nwindowUpdate(nw, &bqoutput);
    }
    return rc;
}

static Result _nwindowDisconnect(NWindow* nw)
{
    Result rc = bqDisconnect(&nw->bq, NATIVE_WINDOW_API_CPU);
    if (R_SUCCEEDED(rc)) {
        nw->is_connected = false;
        nw->slots_configured = 0;
        nw->slots_requested = 0;
        nw->cur_slot = -1;
        nw->width = 0;
        nw->height = 0;
        nw->format = 0;
        nw->usage = 0;
    }
    return rc;
}

bool nwindowIsValid(NWindow* nw)
{
    return nw && nw->magic == NWINDOW_MAGIC;
}

Result nwindowCreate(NWindow* nw, Service* binder_session, s32 binder_id, bool producer_controlled_by_app)
{
    Result rc;

    memset(nw, 0, sizeof(*nw));
    nw->magic = NWINDOW_MAGIC;
    nw->swap_interval = 1;
    nw->cur_slot = -1;
    nw->format = ~0U;
    nw->producer_controlled_by_app = producer_controlled_by_app;

    binderCreate(&nw->bq, binder_id);
    rc = binderInitSession(&nw->bq, binder_session);

    if (R_SUCCEEDED(rc))
        binderGetNativeHandle(&nw->bq, 0x0f, &nw->event);

    if (R_SUCCEEDED(rc))
        rc = _nwindowConnect(nw);

    if (R_FAILED(rc))
        nwindowClose(nw);

    return rc;
}

Result nwindowCreateFromLayer(NWindow* nw, const ViLayer* layer)
{
    return nwindowCreate(nw, viGetSession_IHOSBinderDriverRelay(), layer->igbp_binder_obj_id, false);
}

void nwindowClose(NWindow* nw)
{
    if (!nwindowIsValid(nw))
        return;

    if (nw->is_connected)
        _nwindowDisconnect(nw);

    eventClose(&nw->event);
    binderClose(&nw->bq);

    memset(nw, 0, sizeof(*nw));
}

Result nwindowGetDimensions(NWindow* nw, u32* out_width, u32* out_height)
{
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);
    if (!out_width || !out_height)
        return MAKERESULT(Module_Libnx, LibnxError_BadInput);

    *out_width = nw->width ? nw->width : nw->default_width;
    *out_height = nw->height ? nw->height : nw->default_height;
    return 0;
}

Result nwindowSetDimensions(NWindow* nw, u32 width, u32 height)
{
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);
    if ((nw->width || nw->height) && nw->slots_configured)
        return MAKERESULT(Module_Libnx, LibnxError_AlreadyInitialized);

    nw->width = width;
    nw->height = height;
    memset(&nw->crop, 0, sizeof(nw->crop));
    return 0;
}

Result nwindowSetCrop(NWindow* nw, s32 left, s32 top, s32 right, s32 bottom)
{
    if (right < left || bottom < top)
        return MAKERESULT(Module_Libnx, LibnxError_BadInput);

    u32 width, height;
    Result rc = nwindowGetDimensions(nw, &width, &height);
    if (R_SUCCEEDED(rc)) {
        nw->crop.left   = left < 0 ? 0 : left;
        nw->crop.top    = top < 0 ? 0 : top;
        nw->crop.right  = right > width ? width : right;
        nw->crop.bottom = bottom > height ? height : bottom;
    }
    return rc;
}

Result nwindowSetTransform(NWindow* nw, u32 transform)
{
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);
    if (transform & ~(HAL_TRANSFORM_FLIP_H|HAL_TRANSFORM_FLIP_V|HAL_TRANSFORM_ROT_90))
        return MAKERESULT(Module_Libnx, LibnxError_BadInput);

    nw->transform = transform;
    return 0;
}

Result nwindowSetSwapInterval(NWindow* nw, u32 swap_interval)
{
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);

    nw->swap_interval = swap_interval;
    return 0;
}

Result nwindowConfigureBuffer(NWindow* nw, s32 slot, NvGraphicBuffer* buf)
{
    if (!nw || !buf || slot < 0 || slot >= 64)
        return MAKERESULT(Module_Libnx, LibnxError_BadInput);
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);

    mutexLock(&nw->mutex);

    if (nw->slots_configured & (1UL << slot)) {
        mutexUnlock(&nw->mutex);
        return MAKERESULT(Module_Libnx, LibnxError_AlreadyInitialized);
    }

    if (!nw->is_connected) {
        Result rc = _nwindowConnect(nw);
        if (R_FAILED(rc)) {
            mutexUnlock(&nw->mutex);
            return rc;
        }
    }

    if (!nw->width)
        nw->width = buf->planes[0].width;
    if (!nw->height)
        nw->height = buf->planes[0].height;
    if (nw->format == ~0U)
        nw->format = buf->format;
    if (!nw->usage)
        nw->usage = buf->usage;

    BqGraphicBuffer bqbuf;
    bqbuf.width = nw->width;
    bqbuf.height = nw->height;
    bqbuf.stride = buf->stride;
    //bqbuf.stride = buf->planes[0].pitch / (u8)(buf->planes[0].color_format >> 3); // this also works
    bqbuf.format = nw->format;
    bqbuf.usage = nw->usage;
    bqbuf.native_handle = &buf->header;

    Result rc = bqSetPreallocatedBuffer(&nw->bq, slot, &bqbuf);
    if (R_SUCCEEDED(rc))
        nw->slots_configured |= 1UL << slot;

    mutexUnlock(&nw->mutex);
    return rc;
}

Result nwindowDequeueBuffer(NWindow* nw, s32* out_slot, NvMultiFence* out_fence)
{
    if (!nw)
        return MAKERESULT(Module_Libnx, LibnxError_BadInput);
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);

    mutexLock(&nw->mutex);

    if (!nw->slots_configured || nw->cur_slot >= 0) {
        mutexUnlock(&nw->mutex);
        return MAKERESULT(Module_Libnx, LibnxError_BadGfxDequeueBuffer);
    }

    NvMultiFence fence;
    s32 slot;
    Result rc;

    if (eventActive(&nw->event)) {
        do {
            eventWait(&nw->event, UINT64_MAX);
            rc = bqDequeueBuffer(&nw->bq, true, nw->width, nw->height, nw->format, nw->usage, &slot, &fence);
        } while (R_VALUE(rc) == MAKERESULT(Module_LibnxBinder, LibnxBinderError_WouldBlock));
    }
    else
        rc = bqDequeueBuffer(&nw->bq, false, nw->width, nw->height, nw->format, nw->usage, &slot, &fence);

    if (R_SUCCEEDED(rc)) {
        if (!(nw->slots_requested & (1UL << slot))) {
            rc = bqRequestBuffer(&nw->bq, slot, NULL);
            if (R_FAILED(rc))
                bqCancelBuffer(&nw->bq, slot, &fence);
            else
                nw->slots_requested |= 1UL << slot;
        }
    }

    if (R_SUCCEEDED(rc)) {
        nw->cur_slot = slot;
        if (out_slot)
            *out_slot = slot;
        if (out_fence)
            *out_fence = fence;
        else
            nvMultiFenceWait(&fence, -1);
    }

    mutexUnlock(&nw->mutex);
    return rc;
}

Result nwindowCancelBuffer(NWindow* nw, s32 slot, const NvMultiFence* fence)
{
    if (!nw || slot < 0 || slot >= 64)
        return MAKERESULT(Module_Libnx, LibnxError_BadInput);
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);

    mutexLock(&nw->mutex);

    if (slot != nw->cur_slot) {
        mutexUnlock(&nw->mutex);
        return MAKERESULT(Module_Libnx, LibnxError_BadGfxQueueBuffer);
    }

    static const NvMultiFence s_emptyFence = {0};
    if (!fence)
        fence = &s_emptyFence;

    Result rc = bqCancelBuffer(&nw->bq, slot, fence);
    if (R_SUCCEEDED(rc))
        nw->cur_slot = -1;

    mutexUnlock(&nw->mutex);
    return rc;
}

Result nwindowQueueBuffer(NWindow* nw, s32 slot, const NvMultiFence* fence)
{
    if (!nw || slot < 0 || slot >= 64)
        return MAKERESULT(Module_Libnx, LibnxError_BadInput);
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);

    mutexLock(&nw->mutex);

    if (slot != nw->cur_slot) {
        mutexUnlock(&nw->mutex);
        return MAKERESULT(Module_Libnx, LibnxError_BadGfxQueueBuffer);
    }

    BqBufferInput bqinput;
    memset(&bqinput, 0, sizeof(bqinput));
    bqinput.crop = nw->crop;
    bqinput.scalingMode = nw->scaling_mode;
    bqinput.transform = nw->transform;
    bqinput.stickyTransform = nw->sticky_transform;
    bqinput.swapInterval = nw->swap_interval;
    if (fence)
        bqinput.fence = *fence;

    BqBufferOutput bqoutput;
    Result rc = bqQueueBuffer(&nw->bq, slot, &bqinput, &bqoutput);
    if (R_SUCCEEDED(rc)) {
        nw->cur_slot = -1;
        _nwindowUpdate(nw, &bqoutput);
    }

    mutexUnlock(&nw->mutex);
    return rc;
}

Result nwindowReleaseBuffers(NWindow* nw)
{
    if (!nwindowIsValid(nw))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);

    Result rc = 0;
    mutexLock(&nw->mutex);

    if (nw->cur_slot >= 0)
        rc = MAKERESULT(Module_Libnx, LibnxError_BadInput);
    else if (nw->is_connected && nw->slots_configured)
        rc = _nwindowDisconnect(nw);

    mutexUnlock(&nw->mutex);
    return rc;
}