/**
 * Copyright 2019 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 * 
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 * 
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */

#include "drm_gbm.h"

WEAK uint64_t
gbm_bo_get_modifier(struct gbm_bo *bo);

WEAK int
gbm_bo_get_plane_count(struct gbm_bo *bo);

WEAK uint32_t
gbm_bo_get_stride_for_plane(struct gbm_bo *bo, int plane);

WEAK uint32_t
gbm_bo_get_offset(struct gbm_bo *bo, int plane);

typedef struct {
    struct gbm_bo *bo;
    uint32_t fb_id;
} DRM_FB;

static void page_flip_handler(int fd, unsigned int frame,
          unsigned int sec, unsigned int usec, void *data)
{
    /* suppress 'unused parameter' warnings */
    (void)fd, (void)frame, (void)sec, (void)usec;

    int *waiting_for_flip = data;
    *waiting_for_flip = 0;
}

static drmEventContext drm_event_ctx = {
            .version = DRM_EVENT_CONTEXT_VERSION,
            .page_flip_handler = page_flip_handler,
    };

static void drm_fb_destroy_callback(struct gbm_bo *bo, void *data)
{
    struct gbm_device * gbmDev = gbm_bo_get_device(bo);
	int drm_fd = gbm_device_get_fd(gbmDev);
	DRM_FB *fb = data;

	if (fb->fb_id) {
		drmModeRmFB(drm_fd, fb->fb_id);
        fb->fb_id = 0;
        fb->bo = NULL;
    }

	free(fb);
}

static DRM_FB * drm_fb_get_from_bo2(int drmFd, struct gbm_bo *bo)
{
	DRM_FB *fb = gbm_bo_get_user_data(bo);
	uint32_t width, height, format,
		 strides[4] = {0}, handles[4] = {0},
		 offsets[4] = {0}, flags = 0;
	int ret = -1;

	if (fb) {
		return fb;
    }

	fb = calloc(1, sizeof *fb);
	fb->bo = bo;

	width = gbm_bo_get_width(bo);
	height = gbm_bo_get_height(bo);
	format = gbm_bo_get_format(bo);

	if (gbm_bo_get_modifier && gbm_bo_get_plane_count &&
	    gbm_bo_get_stride_for_plane && gbm_bo_get_offset) {

		uint64_t modifiers[4] = {0};
		modifiers[0] = gbm_bo_get_modifier(bo);
		const int num_planes = gbm_bo_get_plane_count(bo);
		for (int i = 0; i < num_planes; i++) {
			strides[i] = gbm_bo_get_stride_for_plane(bo, i);
			handles[i] = gbm_bo_get_handle(bo).u32;
			offsets[i] = gbm_bo_get_offset(bo, i);
			modifiers[i] = modifiers[0];
		}

		if (modifiers[0]) {
			flags = DRM_MODE_FB_MODIFIERS;
			DBG_PRINT("Using modifier %" PRIx64 "\n", modifiers[0]);
        }

        ret = drmModeAddFB2WithModifiers(drmFd, width, height,
                format, handles, strides, offsets,
                modifiers, &fb->fb_id, flags);
        if(ret) {
            ERR_PRINT("drmModeAddFB2WithModifiers failed!\n");
        } else {
            DBG_PRINT("drmModeAddFB2WithModifiers OK!\n");
        }
	}

	if (ret) {
		memcpy(handles, (uint32_t [4]){gbm_bo_get_handle(bo).u32,0,0,0}, 16);
		memcpy(strides, (uint32_t [4]){gbm_bo_get_stride(bo),0,0,0}, 16);
		memset(offsets, 0, 16);
		ret = drmModeAddFB2(drmFd, width, height, format,
				handles, strides, offsets, &fb->fb_id, 0);
        if(ret) {
            ERR_PRINT("drmModeAddFB2 failed!\n");
        } else {
            DBG_PRINT("drmModeAddFB2 OK!\n");
        }
	}

	if (ret) {
		ERR_PRINT("failed to create fb: %s\n", strerror(errno));
		free(fb);
		return NULL;
	}

	gbm_bo_set_user_data(bo, fb, drm_fb_destroy_callback);

	return fb;
}

static DRM_FB * drm_fb_get_from_bo(int drmFd, struct gbm_bo *bo)
{
    DRM_FB *fb = gbm_bo_get_user_data(bo);
    uint32_t width, height, stride, handle;
    int ret;

    if (fb) {
        return fb;
    }

    fb = calloc(1, sizeof *fb);
    fb->bo = bo;

    width = gbm_bo_get_width(bo);
    height = gbm_bo_get_height(bo);
    stride = gbm_bo_get_stride(bo);
    handle = gbm_bo_get_handle(bo).u32;

    ret = drmModeAddFB(drmFd, width, height, 24, 32, stride, handle, &fb->fb_id);
    if (ret) {
        ERR_PRINT("failed to create fb: %s\n", strerror(errno));
        free(fb);
        return NULL;
    } else {
        DBG_PRINT("drmModeAddFB OK!\n");
    }

    gbm_bo_set_user_data(bo, fb, drm_fb_destroy_callback);

    return fb;
}

JNIEXPORT jlong JNICALL Java_jogamp_newt_driver_egl_gbm_WindowDriver_FirstSwapSurface0
  (JNIEnv *env, jobject obj, jint drmFd, jint jcrtc_id, jint jsurfaceOffsetX, jint jsurfaceOffsetY, 
   jint jconnector_id, jobject jmode, jint jmode_byte_offset, jlong jgbmSurface)
{
    const uint32_t crtc_id = (uint32_t)jcrtc_id;
    const uint32_t surfaceOffsetX = (uint32_t)jsurfaceOffsetX;
    const uint32_t surfaceOffsetY = (uint32_t)jsurfaceOffsetY;
    uint32_t connector_id = (uint32_t)jconnector_id;
    drmModeModeInfo *drmMode = NULL;
    struct gbm_surface *gbmSurface = (struct gbm_surface *) (intptr_t) jgbmSurface;
    struct gbm_bo *nextBO = NULL;
    DRM_FB *fb = NULL;
    int ret;

    if ( NULL != jmode ) {
        const char * c1 = (const char *) (*env)->GetDirectBufferAddress(env, jmode);
        drmMode = (drmModeModeInfo *) (intptr_t) ( c1 + jmode_byte_offset );
    }
    nextBO = gbm_surface_lock_front_buffer(gbmSurface);
    fb = drm_fb_get_from_bo(drmFd, nextBO);
    if (!fb) {
        ERR_PRINT("Failed to get a new framebuffer BO (0)\n");
        return 0;
    }
    /**
     * Set Mode 
     *
     * Fails with x/y != 0: -28 No space left on device
     *   drmModeSetCrtc.0 failed to set mode: fd 26, crtc_id 0x27, fb_id 0x54, pos 10/10, conn_id 0x4d, curMode 1920x1080: -28 No space left on device
     *
     * See https://lists.freedesktop.org/archives/dri-devel/2014-February/053826.html:
     *
     * - The X,Y in drmModeSetCrtc does in fact specify the "source" offset into
     *   your framebuffer (not the destination on the CRTC) which is what I was looking for.
     *
     * - We were able to allocate both a Dumb buffer, and a GBM buffer the size of
     *   6 1920x1200 monitors in a 1x6 configuration, so basically we didn't have
     *   any memory issues with a single buffer that big.
     *
     * - drmModeSetCrtc worked with both Dumb and GBM buffers, and we didn't have
     *   to do anything special on our end, so it clearly is handling the tiling
     *   issues behind the scenes (woot).
     */
    ret = drmModeSetCrtc(drmFd, crtc_id, fb->fb_id, surfaceOffsetX, surfaceOffsetY,
                         &connector_id, 1, drmMode);
    if (ret) {
        ERR_PRINT("drmModeSetCrtc.0 failed to set mode: fd %d, crtc_id 0x%x, fb_id 0x%x (offset %d/%d), conn_id 0x%x, curMode %s: %d %s\n", 
            drmFd, crtc_id, fb->fb_id, surfaceOffsetX, surfaceOffsetY, connector_id, drmMode->name, ret, strerror(errno));
        return 0;
    }
    DBG_PRINT( "EGL_GBM.Window FirstSwapSurface0 nextBO %p, fd %d, crtc_id 0x%x, fb_id 0x%x (offset %d/%d), conn_id 0x%x, curMode %s\n", 
        nextBO, drmFd, crtc_id, fb->fb_id, surfaceOffsetX, surfaceOffsetY, connector_id, drmMode->name);
    return (jlong) (intptr_t) nextBO;
}

#ifdef VERBOSE_ON
static int nextSwapVerboseOnce = 1;
#endif

JNIEXPORT jlong JNICALL Java_jogamp_newt_driver_egl_gbm_WindowDriver_NextSwapSurface0
  (JNIEnv *env, jobject obj, jint drmFd, jint jcrtc_id, 
   jint jconnector_id, jobject jmode, jint jmode_byte_offset, 
   jlong jgbmSurface, jlong jlastBO, jint swapInterval)
{
    const uint32_t crtc_id = (uint32_t)jcrtc_id;
    const uint32_t connector_id = (uint32_t)jconnector_id;
    drmModeModeInfo *drmMode = NULL;
    struct gbm_surface *gbmSurface = (struct gbm_surface *) (intptr_t) jgbmSurface;
    struct gbm_bo *lastBO = (struct gbm_bo*) (intptr_t) jlastBO;
    struct gbm_bo *nextBO = NULL;
    DRM_FB *fbNext = NULL;
    int ret, waiting_for_flip = 1;
    fd_set fds;

    if ( NULL != jmode ) {
        const char * c1 = (const char *) (*env)->GetDirectBufferAddress(env, jmode);
        drmMode = (drmModeModeInfo *) (intptr_t) ( c1 + jmode_byte_offset );
    }
    nextBO = gbm_surface_lock_front_buffer(gbmSurface);
    fbNext = drm_fb_get_from_bo(drmFd, nextBO);
    if (!fbNext) {
        ERR_PRINT("Failed to get a new framebuffer BO (1)\n");
        return 0;
    }
    if( 0 != swapInterval) {
        // https://github.com/dvdhrm/docs/blob/master/drm-howto/modeset-vsync.c#L614
        ret = drmModePageFlip(drmFd, crtc_id, fbNext->fb_id,
                DRM_MODE_PAGE_FLIP_EVENT, &waiting_for_flip);
        if (ret) {
            ERR_PRINT("drmModePageFlip.1 failed to queue page flip: fd %d, crtc_id 0x%x, fb_id 0x%x, conn_id 0x%x, curMode %s: %p -> %p: %d %s\n", 
                drmFd, crtc_id, fbNext->fb_id, connector_id, drmMode->name, lastBO, nextBO, ret, strerror(errno));
            return 0;
        }

        while (waiting_for_flip) {
            FD_ZERO(&fds);
            FD_SET(drmFd, &fds);

            ret = select(drmFd + 1, &fds, NULL, NULL, NULL);
            if (ret < 0) {
                ERR_PRINT("drm.select: select err: %s\n", strerror(errno));
                return ret;
            } else if (ret == 0) {
                ERR_PRINT("drm.select: select timeout!\n");
                return -1;
            }
            drmHandleEvent(drmFd, &drm_event_ctx);
        }
    }

    /* release last buffer to render on again: */
    if( NULL != lastBO ) {
        gbm_surface_release_buffer(gbmSurface, lastBO);
    }

#ifdef VERBOSE_ON
    if( nextSwapVerboseOnce ) {
        nextSwapVerboseOnce = 0;
        DBG_PRINT( "EGL_GBM.Window NextSwapSurface0 swapInterval %d, bo %p -> %p, fd %d, crtc_id 0x%x, fb_id 0x%x, conn_id 0x%x, curMode %s\n", 
            swapInterval, lastBO, nextBO, drmFd, crtc_id, fbNext->fb_id, connector_id, drmMode->name);
    }
#endif
    return (jlong) (intptr_t) nextBO;
}