/* Copyright (C) 2007-2008 The Android Open Source Project
**
** This software is licensed under the terms of the GNU General Public
** License version 2, as published by the Free Software Foundation, and
** may be copied, distributed, and modified under those terms.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
*/
#include "vl.h"
#include "android.h"
#include "goldfish_device.h"
#include "framebuffer.h"

enum {
    FB_GET_WIDTH        = 0x00,
    FB_GET_HEIGHT       = 0x04,
    FB_INT_STATUS       = 0x08,
    FB_INT_ENABLE       = 0x0c,
    FB_SET_BASE         = 0x10,
    FB_SET_ROTATION     = 0x14,
    FB_SET_BLANK        = 0x18,
    FB_GET_PHYS_WIDTH   = 0x1c,
    FB_GET_PHYS_HEIGHT  = 0x20,

    FB_INT_VSYNC             = 1U << 0,
    FB_INT_BASE_UPDATE_DONE  = 1U << 1
};

struct goldfish_fb_state {
    struct goldfish_device dev;
    QFrameBuffer*  qfbuff;
    uint32_t fb_base;
    uint32_t base_valid : 1;
    uint32_t need_update : 1;
    uint32_t need_int : 1;
    uint32_t set_rotation : 2;
    uint32_t blank : 1;
    uint32_t int_status;
    uint32_t int_enable;
    int      rotation;   /* 0, 1, 2 or 3 */
};

#define  GOLDFISH_FB_SAVE_VERSION  1

static void goldfish_fb_save(QEMUFile*  f, void*  opaque)
{
    struct goldfish_fb_state*  s = opaque;

    QFrameBuffer*  q = s->qfbuff;

    qemu_put_be32(f, q->width);
    qemu_put_be32(f, q->height);
    qemu_put_be32(f, q->pitch);
    qemu_put_byte(f, q->rotation);

    qemu_put_be32(f, s->fb_base);
    qemu_put_byte(f, s->base_valid);
    qemu_put_byte(f, s->need_update);
    qemu_put_byte(f, s->need_int);
    qemu_put_byte(f, s->set_rotation);
    qemu_put_byte(f, s->blank);
    qemu_put_be32(f, s->int_status);
    qemu_put_be32(f, s->int_enable);
    qemu_put_be32(f, s->rotation);
}

static int  goldfish_fb_load(QEMUFile*  f, void*  opaque, int  version_id)
{
    struct goldfish_fb_state*  s   = opaque;

    QFrameBuffer*              q   = s->qfbuff;
    int                        ret = -1;
    int                        ds_w, ds_h, ds_pitch, ds_rot;

    if (version_id != GOLDFISH_FB_SAVE_VERSION)
        goto Exit;

    ds_w     = qemu_get_be32(f);
    ds_h     = qemu_get_be32(f);
    ds_pitch = qemu_get_be32(f);
    ds_rot   = qemu_get_byte(f);

    if (q->width != ds_w      ||
        q->height != ds_h     ||
        q->pitch != ds_pitch  ||
        q->rotation != ds_rot )
    {
        /* XXX: We should be able to force a resize/rotation from here ? */
        fprintf(stderr, "%s: framebuffer dimensions mismatch\n", __FUNCTION__);
        goto Exit;
    }

    s->fb_base      = qemu_get_be32(f);
    s->base_valid   = qemu_get_byte(f);
    s->need_update  = qemu_get_byte(f);
    s->need_int     = qemu_get_byte(f);
    s->set_rotation = qemu_get_byte(f);
    s->blank        = qemu_get_byte(f);
    s->int_status   = qemu_get_be32(f);
    s->int_enable   = qemu_get_be32(f);
    s->rotation     = qemu_get_be32(f);

    /* force a refresh */
    s->need_update = 1;

    ret = 0;
Exit:
    return ret;
}


#define  STATS  0

#if STATS
static int   stats_counter;
static long  stats_total;
static int   stats_full_updates;
static long  stats_total_full_updates;
#endif

static void goldfish_fb_update_display(void *opaque)
{
    struct goldfish_fb_state *s = (struct goldfish_fb_state *)opaque;
    uint32_t addr;
    uint32_t base;

    uint8_t*  dst_line;
    uint8_t*  src_line;
    int y_first, y_last = 0;
    int full_update = 0;
    int    width, height, pitch;

    base = s->fb_base;
    if(base == 0)
        return;

    if((s->int_enable & FB_INT_VSYNC) && !(s->int_status & FB_INT_VSYNC)) {
        s->int_status |= FB_INT_VSYNC;
        goldfish_device_set_irq(&s->dev, 0, 1);
    }

    y_first = -1;
    addr  = base;
    if(s->need_update) {
        full_update = 1;
        if(s->need_int) {
            s->int_status |= FB_INT_BASE_UPDATE_DONE;
            if(s->int_enable & FB_INT_BASE_UPDATE_DONE)
                goldfish_device_set_irq(&s->dev, 0, 1);
        }
        s->need_int = 0;
        s->need_update = 0;
    }

    src_line = phys_ram_base + base;
    dst_line  = s->qfbuff->pixels;
    pitch     = s->qfbuff->pitch;
    width     = s->qfbuff->width;
    height    = s->qfbuff->height;

#if STATS
    if (full_update)
        stats_full_updates += 1;
    if (++stats_counter == 120) {
        stats_total               += stats_counter;
        stats_total_full_updates  += stats_full_updates;

        printf( "full update stats:  peak %.2f %%  total %.2f %%\n",
                stats_full_updates*100.0/stats_counter,
                stats_total_full_updates*100.0/stats_total );

        stats_counter      = 0;
        stats_full_updates = 0;
    }
#endif /* STATS */

    if (s->blank)
    {
        memset( dst_line, 0, height*pitch );
        y_first = 0;
        y_last  = height-1;
    }
    else if (full_update)
    {
        int  yy;

        for (yy = 0; yy < height; yy++, dst_line += pitch, src_line += width*2)
        {
            uint16_t*  src = (uint16_t*) src_line;
            uint16_t*  dst = (uint16_t*) dst_line;
            int        nn;

            for (nn = 0; nn < width; nn++) {
                unsigned   spix = src[nn];
                unsigned   dpix = dst[nn];
#if WORDS_BIGENDIAN
                spix = ((spix << 8) | (spix >> 8)) & 0xffff;
#else
                if (spix != dpix)
                    break;
#endif
            }

            if (nn == width)
                continue;

#if WORDS_BIGENDIAN
            for ( ; nn < width; nn++ ) {
                unsigned   spix = src[nn];
                dst[nn] = (uint16_t)((spix << 8) | (spix >> 8));
            }
#else
            memcpy( dst+nn, src+nn, (width-nn)*2 );
#endif

            y_first = (y_first < 0) ? yy : y_first;
            y_last  = yy;
        }
    }
    else  /* not a full update, should not happen very often with Android */
    {
        int  yy;

        for (yy = 0; yy < height; yy++, dst_line += pitch, src_line += width*2)
        {
            uint16_t*  src   = (uint16_t*) src_line;
            uint16_t*  dst   = (uint16_t*) dst_line;
            int        len   = width*2;
#if WORDS_BIGENDIAN
            int        nn;
#endif
            int        dirty = 0;

            while (len > 0) {
                int  len2 = TARGET_PAGE_SIZE - (addr & (TARGET_PAGE_SIZE-1));

                if (len2 > len)
                    len2 = len;

                dirty |= cpu_physical_memory_get_dirty(addr, VGA_DIRTY_FLAG);
                addr  += len2;
                len   -= len2;
            }

            if (!dirty)
                continue;

#if WORDS_BIGENDIAN
            for (nn = 0; nn < width; nn++ ) {
                unsigned   spix = src[nn];
                dst[nn] = (uint16_t)((spix << 8) | (spix >> 8));
            }
#else
            memcpy( dst, src, width*2 );
#endif

            y_first = (y_first < 0) ? yy : y_first;
            y_last  = yy;
        }
    }

    if (y_first < 0)
      return;

    y_last += 1;
    //printf("goldfish_fb_update_display %d %d, base %x\n", first, last, base);

    cpu_physical_memory_reset_dirty(base + y_first * width * 2,
                                    base + y_last * width * 2,
                                    VGA_DIRTY_FLAG);

    qframebuffer_update( s->qfbuff, 0, y_first, width, y_last-y_first );
}

static void goldfish_fb_invalidate_display(void * opaque)
{
    // is this called?
    struct goldfish_fb_state *s = (struct goldfish_fb_state *)opaque;
    s->need_update = 1;
}

static void  goldfish_fb_detach_display(void*  opaque)
{
    struct goldfish_fb_state *s = (struct goldfish_fb_state *)opaque;
    s->qfbuff = NULL;
}

static uint32_t goldfish_fb_read(void *opaque, target_phys_addr_t offset)
{
    uint32_t ret;
    struct goldfish_fb_state *s = opaque;
    offset -= s->dev.base;
    switch(offset) {
        case FB_GET_WIDTH:
            ret = s->qfbuff->width;
            //printf("FB_GET_WIDTH => %d\n", ret);
            return ret;

        case FB_GET_HEIGHT:
            ret = s->qfbuff->height;
            //printf( "FB_GET_HEIGHT = %d\n", ret);
            return ret;

        case FB_INT_STATUS:
            ret = s->int_status & s->int_enable;
            if(ret) {
                s->int_status &= ~ret;
                goldfish_device_set_irq(&s->dev, 0, 0);
            }
            return ret;

        case FB_GET_PHYS_WIDTH:
            ret = s->qfbuff->phys_width_mm;
            //printf( "FB_GET_PHYS_WIDTH => %d\n", ret );
            return ret;

        case FB_GET_PHYS_HEIGHT:
            ret = s->qfbuff->phys_height_mm;
            //printf( "FB_GET_PHYS_HEIGHT => %d\n", ret );
            return ret;

        default:
            cpu_abort (cpu_single_env, "goldfish_fb_read: Bad offset %x\n", offset);
            return 0;
    }
}

static void goldfish_fb_write(void *opaque, target_phys_addr_t offset,
                        uint32_t val)
{
    struct goldfish_fb_state *s = opaque;
    offset -= s->dev.base;
    switch(offset) {
        case FB_INT_ENABLE:
            s->int_enable = val;
            goldfish_device_set_irq(&s->dev, 0, (s->int_status & s->int_enable));
            break;
        case FB_SET_BASE: {
            int need_resize = !s->base_valid;
            s->fb_base = val;
            s->int_status &= ~FB_INT_BASE_UPDATE_DONE;
            s->need_update = 1;
            s->need_int = 1;
            s->base_valid = 1;
            if(s->set_rotation != s->rotation) {
                //printf("FB_SET_BASE: rotation : %d => %d\n", s->rotation, s->set_rotation);
                s->rotation = s->set_rotation;
                need_resize = 1;
            }
            goldfish_device_set_irq(&s->dev, 0, (s->int_status & s->int_enable));
            if (need_resize) {
                //printf("FB_SET_BASE: need resize (rotation=%d)\n", s->rotation );
                qframebuffer_rotate( s->qfbuff, s->rotation );
            }
            } break;
        case FB_SET_ROTATION:
            //printf( "FB_SET_ROTATION %d\n", val);
            s->set_rotation = val;
            break;
        case FB_SET_BLANK:
            s->blank = val;
            s->need_update = 1;
            break;
        default:
            cpu_abort (cpu_single_env, "goldfish_fb_write: Bad offset %x\n", offset);
    }
}

static CPUReadMemoryFunc *goldfish_fb_readfn[] = {
   goldfish_fb_read,
   goldfish_fb_read,
   goldfish_fb_read
};

static CPUWriteMemoryFunc *goldfish_fb_writefn[] = {
   goldfish_fb_write,
   goldfish_fb_write,
   goldfish_fb_write
};

void goldfish_fb_init(DisplayState *ds, int id)
{
    struct goldfish_fb_state *s;

    s = (struct goldfish_fb_state *)qemu_mallocz(sizeof(*s));
    s->dev.name = "goldfish_fb";
    s->dev.id = id;
    s->dev.size = 0x1000;
    s->dev.irq_count = 1;

    s->qfbuff = qframebuffer_fifo_get();
    qframebuffer_set_producer( s->qfbuff, s,
                               goldfish_fb_update_display,
                               goldfish_fb_invalidate_display,
                               goldfish_fb_detach_display );

    goldfish_device_add(&s->dev, goldfish_fb_readfn, goldfish_fb_writefn, s);

    register_savevm( "goldfish_fb", 0, GOLDFISH_FB_SAVE_VERSION,
                     goldfish_fb_save, goldfish_fb_load, s);
}

