/*
 * display.c
 *
 * ============================================================================
 * Copyright (c) Texas Instruments Inc 2005
 *
 * Use of this software is controlled by the terms and conditions found in the
 * license agreement under which this software has been supplied or provided.
 * ============================================================================
 */

/* Standard Linux headers */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <linux/fb.h>

/* Davinci specific kernel headers */
#include <video/davincifb.h>

/* Demo headers */
#include <rendezvous.h>
#include <fifoutil.h>
#include "rotate_demo.h"
#include "display.h"

/* Video display is triple buffered */
#define NUM_BUFS                 3

/* Black color in UYVY format */
#define UYVY_BLACK               0x10801080

/* The levels of initialization */
#define DISPLAYDEVICEINITIALIZED 0x1

static int waitForVsync(int fd);
static int flipDisplayBuffers(int fd, int displayIdx);
static int initDisplayDevice(char *displays[]);
static void cleanupDisplayDevice(int fd, char *displays[]);


/******************************************************************************
 * waitForVsync
 ******************************************************************************/
static int waitForVsync(int fd)
{
    int dummy;

    /* Wait for vertical sync */
    if (ioctl(fd, FBIO_WAITFORVSYNC, &dummy) == -1) {
        ERR("Failed FBIO_WAITFORVSYNC (%s)\n", strerror(errno));
        return FAILURE;
    }

    return SUCCESS;
}

/******************************************************************************
 * flipDisplayBuffers
 ******************************************************************************/
static int flipDisplayBuffers(int fd, int displayIdx)
{
    struct fb_var_screeninfo vInfo;

    if (ioctl(fd, FBIOGET_VSCREENINFO, &vInfo) == -1) {
        ERR("Failed FBIOGET_VSCREENINFO (%s)\n", strerror(errno));
        return FAILURE;
    }

    vInfo.yoffset = vInfo.yres * displayIdx;

    /* Swap the working buffer for the displayed buffer */
    if (ioctl(fd, FBIOPAN_DISPLAY, &vInfo) == -1) {
        ERR("Failed FBIOPAN_DISPLAY (%s)\n", strerror(errno));
        return FAILURE;
    }

    return SUCCESS;
}

/******************************************************************************
 * initDisplayDevice
 ******************************************************************************/
static int initDisplayDevice(char *displays[])
{
    struct fb_var_screeninfo varInfo;
    unsigned int            *buf;
    int                      fd;
    int                      i;

    /* Open video display device */
    fd = open(FBVID_DEVICE, O_RDWR);

    if (fd == -1) {
        ERR("Failed to open fb device %s (%s)\n", FBVID_DEVICE,
                                                  strerror(errno));
        return FAILURE;
    }

    if (ioctl(fd, FBIOGET_VSCREENINFO, &varInfo) == -1) {
        ERR("Failed FBIOGET_VSCREENINFO on %s (%s)\n", FBVID_DEVICE,
                                                       strerror(errno));
        return FAILURE;
    }

    varInfo.xres = D1_WIDTH;
    varInfo.yres = D1_HEIGHT;
    varInfo.bits_per_pixel = SCREEN_BPP;

    /* Set video display format */
    if (ioctl(fd, FBIOPUT_VSCREENINFO, &varInfo) == -1) {
        ERR("Failed FBIOPUT_VSCREENINFO on %s (%s)\n", FBVID_DEVICE,
                                                       strerror(errno));
        return FAILURE;
    }

    if (varInfo.xres != D1_WIDTH ||
        varInfo.yres != D1_HEIGHT ||
        varInfo.bits_per_pixel != SCREEN_BPP) {
        ERR("Failed to get the requested screen size: %dx%d at %d bpp\n",
            D1_WIDTH, D1_HEIGHT, SCREEN_BPP);
        return FAILURE;
    }

    /* Map the video buffers to user space */
    displays[0] = (char *) mmap (NULL,
                                 D1_FRAME_SIZE * NUM_BUFS,
                                 PROT_READ | PROT_WRITE,
                                 MAP_SHARED,
                                 fd, 0);

    if (displays[0] == MAP_FAILED) {
        ERR("Failed mmap on %s (%s)\n", FBVID_DEVICE, strerror(errno));
        return FAILURE;
    }

    /* Clear the video buffers */
    buf = (unsigned int *) displays[0];

    for (i=0; i<D1_FRAME_SIZE * NUM_BUFS / sizeof(unsigned int); i++) {
        buf[i] = UYVY_BLACK;
    }

    DBG("Display buffer %d mapped to address %#lx\n", 0,
        (unsigned long) displays[0]);

    for (i=0; i<NUM_BUFS-1; i++) {
        displays[i+1] = displays[i] + D1_FRAME_SIZE;
        DBG("Display buffer %d mapped to address %#lx\n", i+1,
            (unsigned long) displays[i+1]);
    }

    return fd;
}

/******************************************************************************
 * cleanupDisplayDevice
 ******************************************************************************/
static void cleanupDisplayDevice(int fd, char *displays[])
{
    munmap(displays[0], D1_FRAME_SIZE * NUM_BUFS);
    close(fd);
}

/******************************************************************************
 * copyFrame
 ******************************************************************************/
static void copyFrame(char *dst, char *src, int width, int height)
{
    int lineWidth = width * SCREEN_BPP / 8;
    int xoffset   = (D1_LINE_WIDTH - lineWidth) / 2;
    int ystart    = ( (D1_HEIGHT - height) / 2 ) & ~1;
    int y;

    /* Truncate if frame buffer higher than display buffer */
    if (ystart < 0) {
        ystart = 0;
    }

    dst += ystart * D1_LINE_WIDTH + xoffset;

    /* Copy the frame into the middle of the screen */
    for (y=0; y < height && y < D1_HEIGHT; y++) {
        memcpy(dst, src, lineWidth);
        dst += D1_LINE_WIDTH;
        src += lineWidth;
    }
}

/******************************************************************************
 * displayThrFxn
 ******************************************************************************/
void *displayThrFxn(void *arg)
{
    DisplayEnv     *envp          = (DisplayEnv *) arg;
    BufferElement   flush         = { DISPLAY_FLUSH };
    void           *status        = THREAD_SUCCESS;
    unsigned int    initMask      = 0;
    int             displayIdx    = 0;
    int             fbFd          = 0;
    char           *displays[NUM_BUFS];
    BufferElement   e;

    /* Initialize the video display device */
    fbFd = initDisplayDevice(displays);

    if (fbFd == FAILURE) {
        cleanup(THREAD_FAILURE);
    }

    DBG("Video display device initialized.\n");

    initMask |= DISPLAYDEVICEINITIALIZED;

    /* Signal that initialization is done and wait for other threads */
    Rendezvous_meet(envp->hRendezvous);

    /* Wait for the vertical sync of the display device */
    waitForVsync(fbFd);

    DBG("Entering display main loop.\n");
    while (TRUE) {
        if (!gblGetPlay() && !gblGetQuit()) {
            usleep(PAUSE);
            continue;
        }

        /* Receive a buffer with a decoded frame from the video thread */
        if (FifoUtil_get(&envp->outFifo, &e) == FIFOUTIL_FAILURE) {        
            breakLoop(THREAD_FAILURE);
        }

        /* Is the video thread flushing the pipe? */
        if (e.id == DISPLAY_FLUSH) {
            breakLoop(THREAD_SUCCESS);
        } else if (e.id == DISPLAY_PRIME) {
            pthread_mutex_lock(&envp->prime);
            pthread_mutex_unlock(&envp->prime);
            continue;
        }

        /* Increment the display index */
        displayIdx = (displayIdx + 1) % NUM_BUFS;

        /* Copy the supplied frame to the display frame buffer */
        copyFrame(displays[displayIdx], e.frameBuffer, e.width, e.height);

        /* Give back the buffer to the video thread */
        if (FifoUtil_put(&envp->inFifo, &e) == FIFOUTIL_FAILURE) {
            breakLoop(THREAD_FAILURE);
        }

        /* Flip display buffer and working buffer */
        flipDisplayBuffers(fbFd, displayIdx); 

        /* Wait for the vertical sync of the display device */
        waitForVsync(fbFd);

        gblIncFrames();
    }

cleanup:
    /* Make sure the other threads aren't waiting for init to complete */
    Rendezvous_force(envp->hRendezvous);

    /* Make sure the video thread isn't stuck in FifoUtil_get() */
    FifoUtil_put(&envp->inFifo, &flush);

    /* Clean up the display thread */
    if (initMask & DISPLAYDEVICEINITIALIZED) {
        cleanupDisplayDevice(fbFd, displays);
    }

    return status;
}


