/* 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 "skin_image.h"
#include "android_resource.h"
#include <assert.h>
#include <limits.h>

#define  DEBUG  0

#if DEBUG
static void D(const char*  fmt, ...)
{
    va_list  args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
}
#else
#define  D(...)  do{}while(0)
#endif

/********************************************************************************/
/********************************************************************************/
/*****                                                                      *****/
/*****            U T I L I T Y   F U N C T I O N S                         *****/
/*****                                                                      *****/
/********************************************************************************/
/********************************************************************************/

SDL_Surface*
sdl_surface_from_argb32( void*  base, int  w, int  h )
{
    return SDL_CreateRGBSurfaceFrom(
                        base, w, h, 32, w*4,
#if WORDS_BIGENDIAN
                        0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000
#else
                        0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000
#endif
                        );
}

static void*
rotate_image( void*  data, unsigned  width, unsigned  height,  SkinRotation  rotation )
{
    void*  result;

    result = malloc( width*height*4 );
    if (result == NULL)
        return NULL;

    switch (rotation & 3)
    {
    case SKIN_ROTATION_0:
        memcpy( (char*)result, (const char*)data, width*height*4 );
        break;

    case SKIN_ROTATION_270:
        {
            unsigned*  start    = (unsigned*)data;
            unsigned*  src_line = start + (width-1);
            unsigned*  dst_line = (unsigned*)result;
            unsigned   hh;

            for (hh = width; hh > 0; hh--)
            {
                unsigned*  src   = src_line;
                unsigned*  dst   = dst_line;
                unsigned   count = height;

                for ( ; count > 0; count-- ) {
                    dst[0] = src[0];
                    dst   += 1;
                    src   += width;
                }

                src_line -= 1;
                dst_line += height;
            }
        }
        break;

    case SKIN_ROTATION_180:
        {
            unsigned*  start    = (unsigned*)data;
            unsigned*  src_line = start + width*(height-1);
            unsigned*  dst_line = (unsigned*)result;
            unsigned   hh;

            for (hh = height; hh > 0; hh--)
            {
                unsigned*  src = src_line + (width-1);
                unsigned*  dst = dst_line;

                while (src >= src_line)
                    *dst++ = *src--;

                dst_line += width;
                src_line -= width;
            }
        }
        break;

    case SKIN_ROTATION_90:
        {
            unsigned*  start    = (unsigned*)data;
            unsigned*  src_line = start + width*(height-1);
            unsigned*  dst_line = (unsigned*)result ;
            unsigned   hh;

            for (hh = width; hh > 0; hh--)
            {
                unsigned*  src = src_line;
                unsigned*  dst = dst_line;
                unsigned   count;

                for (count = height; count > 0; count--) {
                    dst[0] = src[0];
                    dst   += 1;
                    src   -= width;
                }

                dst_line += height;
                src_line += 1;
            }
        }
        break;

    default:
        ;
    }

    return result;
}


static void
blend_image( unsigned*  dst_pixels,
             unsigned*  src_pixels,
             unsigned   w,
             unsigned   h,
             int        alpha )
{
    unsigned*  dst     = dst_pixels;
    unsigned*  dst_end = dst + w*h;
    unsigned*  src     = src_pixels;

    for ( ; dst < dst_end; dst++, src++ )
    {
        {
            unsigned  ag = (src[0] >> 8) & 0xff00ff;
            unsigned  rb =  src[0]       & 0xff00ff;

            ag = (ag*alpha) & 0xff00ff00;
            rb = ((rb*alpha) >> 8) & 0x00ff00ff;

            dst[0] = ag | rb;
        }
    }
}


static unsigned
skin_image_desc_hash( SkinImageDesc*  desc )
{
    unsigned  h = 0;
    int       n;

    for (n = 0; desc->path[n] != 0; n++) {
        int  c = desc->path[n];
        h = h*33 + c;
    }
    h += desc->rotation*1573;
    h += desc->blend * 7;

    return  h;
}


static int
skin_image_desc_equal( SkinImageDesc*  a,
                       SkinImageDesc*  b )
{
    return (a->rotation == b->rotation &&
            a->blend    == b->blend    &&
            !strcmp(a->path, b->path));
}

/********************************************************************************/
/********************************************************************************/
/*****                                                                      *****/
/*****            S K I N   I M A G E S                                     *****/
/*****                                                                      *****/
/********************************************************************************/
/********************************************************************************/

enum {
    SKIN_IMAGE_CLONE = (1 << 0)   /* this image is a clone */
};

struct SkinImage {
    unsigned         hash;
    SkinImage*       link;
    int              ref_count;
    SkinImage*       next;
    SkinImage*       prev;
    SDL_Surface*     surface;
    unsigned         flags;
    unsigned         w, h;
    void*            pixels;  /* 32-bit ARGB */
    SkinImageDesc    desc;
};




static const SkinImage  _no_image[1] = {
    { 0, NULL, 0, NULL, NULL, NULL, 0, 0, 0, NULL, { "<none>", SKIN_ROTATION_0, 0 } }
};

SkinImage*  SKIN_IMAGE_NONE = (SkinImage*)&_no_image;

static void
skin_image_free( SkinImage*  image )
{
    if (image && image != _no_image)
    {
        if (image->surface) {
            SDL_FreeSurface(image->surface);
            image->surface = NULL;
        }

        if (image->pixels) {
            free( image->pixels );
            image->pixels = NULL;
        }

        free(image);
    }
}


static SkinImage*
skin_image_alloc( SkinImageDesc*  desc, unsigned  hash )
{
    int         len   = strlen(desc->path);
    SkinImage*  image = calloc(1, sizeof(*image) + len + 1);

    if (image) {
        image->desc = desc[0];
        image->desc.path = (const char*)(image + 1);
        memcpy( (char*)image->desc.path, desc->path, len );
        ((char*)image->desc.path)[len] = 0;

        image->hash      = hash;
        image->next      = image->prev = image;
        image->ref_count = 1;
    }
    return image;
}


extern void *loadpng(const char *fn, unsigned *_width, unsigned *_height);
extern void *readpng(const unsigned char*  base, size_t  size, unsigned *_width, unsigned *_height);

static int
skin_image_load( SkinImage*  image )
{
    void*     data;
    unsigned  w, h;
    const char*  path = image->desc.path;

    if (path[0] == ':') {
        size_t                size;
        const unsigned char*  base;

        if (path[1] == '/' || path[1] == '\\')
            path += 1;

        base = android_resource_find( path+1, &size );
        if (base == NULL) {
            fprintf(stderr, "failed to locate built-in image file '%s'\n", path );
            return -1;
        }

        data = readpng(base, size, &w, &h);
        if (data == NULL) {
            fprintf(stderr, "failed to load built-in image file '%s'\n", path );
            return -1;
        }
    } else {
        data = loadpng(path, &w, &h);
        if (data == NULL) {
            fprintf(stderr, "failed to load image file '%s'\n", path );
            return -1;
        }
    }

   /* the data is loaded into memory as RGBA bytes by libpng. we want to manage
    * the values as 32-bit ARGB pixels, so swap the bytes accordingly depending
    * on our CPU endianess
    */
    {
        unsigned*  d     = data;
        unsigned*  d_end = d + w*h;

        for ( ; d < d_end; d++ ) {
            unsigned  pix = d[0];
#if WORDS_BIGENDIAN
            /* R,G,B,A read as RGBA => ARGB */
            pix = ((pix >> 8) & 0xffffff) | (pix << 24);
#else
            /* R,G,B,A read as ABGR => ARGB */
            pix = (pix & 0xff00ff00) | ((pix >> 16) & 0xff) | ((pix & 0xff) << 16);
#endif
            d[0] = pix;
        }
    }

    image->pixels = data;
    image->w      = w;
    image->h      = h;

    image->surface = sdl_surface_from_argb32( image->pixels, w, h );
    if (image->surface == NULL) {
        fprintf(stderr, "failed to create SDL surface for '%s' image\n", path);
        return -1;
    }
    return 0;
}


/* simple hash table for images */

#define  NUM_BUCKETS  64

typedef struct {
    SkinImage*     buckets[ NUM_BUCKETS ];
    SkinImage      mru_head;
    int            num_images;
    unsigned long  total_pixels;
    unsigned long  max_pixels;
    unsigned long  total_images;
} SkinImageCache;


static void
skin_image_cache_init( SkinImageCache*  cache )
{
    memset(cache, 0, sizeof(*cache));
#if DEBUG
    cache->max_pixels = 1;
#else
    cache->max_pixels = 4*1024*1024;  /* limit image cache to 4 MB */
#endif
    cache->mru_head.next = cache->mru_head.prev = &cache->mru_head;
}


static void
skin_image_cache_remove( SkinImageCache*  cache,
                         SkinImage*       image )
{
    /* remove from hash table */
    SkinImage**  pnode = cache->buckets + (image->hash & (NUM_BUCKETS-1));
    SkinImage*   node;

    for (;;) {
        node = *pnode;
        assert(node != NULL);
        if (node == NULL)  /* should not happen */
            break;
        if (node == image) {
            *pnode = node->link;
            break;
        }
        pnode = &node->link;
    }

    D( "skin_image_cache: remove '%s' (rot=%d), %d pixels\n",
       node->desc.path, node->desc.rotation, node->w*node->h );

    /* remove from mru list */
    image->prev->next = image->next;
    image->next->prev = image->prev;

    cache->total_pixels -= image->w*image->h;
    cache->total_images -= 1;
}


static SkinImage*
skin_image_cache_raise( SkinImageCache*  cache,
                        SkinImage*       image )
{
    if (image != cache->mru_head.next) {
        SkinImage*  prev = image->prev;
        SkinImage*  next = image->next;

        /* remove from mru list */
        prev->next = next;
        next->prev = prev;

        /* add to top */
        image->prev = &cache->mru_head;
        image->next = image->prev->next;
        image->prev->next = image;
        image->next->prev = image;
    }
    return image;
}


static void
skin_image_cache_flush( SkinImageCache*  cache )
{
    SkinImage*     image = cache->mru_head.prev;
    int            count = 0;

    D("skin_image_cache_flush: starting\n");
    while (cache->total_pixels > cache->max_pixels &&
           image != &cache->mru_head)
    {
        SkinImage*  prev = image->prev;

        if (image->ref_count == 0) {
            skin_image_cache_remove(cache, image);
            count += 1;
        }
        image = prev;
    }
    D("skin_image_cache_flush: finished, %d images flushed\n", count);
}


static SkinImage**
skin_image_lookup_p( SkinImageCache*   cache,
                     SkinImageDesc*    desc,
                     unsigned         *phash )
{
    unsigned     h     = skin_image_desc_hash(desc);
    unsigned     index = h & (NUM_BUCKETS-1);
    SkinImage**  pnode = &cache->buckets[index];
    for (;;) {
        SkinImage*  node = *pnode;
        if (node == NULL)
            break;
        if (node->hash == h && skin_image_desc_equal(desc, &node->desc))
            break;
        pnode = &node->link;
    }
    *phash = h;
    return  pnode;
}


static SkinImage*
skin_image_create( SkinImageDesc*  desc, unsigned  hash )
{
    SkinImage*  node;

    node = skin_image_alloc( desc, hash );
    if (node == NULL)
        return SKIN_IMAGE_NONE;

    if (desc->rotation == SKIN_ROTATION_0 &&
        desc->blend    == SKIN_BLEND_FULL)
    {
        if (skin_image_load(node) < 0) {
            skin_image_free(node);
            return SKIN_IMAGE_NONE;
        }
    }
    else
    {
        SkinImageDesc  desc0 = desc[0];
        SkinImage*     parent;

        desc0.rotation = SKIN_ROTATION_0;
        desc0.blend    = SKIN_BLEND_FULL;

        parent = skin_image_find( &desc0 );
        if (parent == SKIN_IMAGE_NONE)
            return SKIN_IMAGE_NONE;

        SDL_LockSurface(parent->surface);

        if (desc->rotation == SKIN_ROTATION_90 ||
            desc->rotation == SKIN_ROTATION_270)
        {
            node->w = parent->h;
            node->h = parent->w;
        } else {
            node->w = parent->w;
            node->h = parent->h;
        }

        node->pixels = rotate_image( parent->pixels, parent->w, parent->h,
                                    desc->rotation );

        SDL_UnlockSurface(parent->surface);
        skin_image_unref(&parent);

        if (node->pixels  == NULL) {
            skin_image_free(node);
            return SKIN_IMAGE_NONE;
        }

        if (desc->blend != SKIN_BLEND_FULL)
            blend_image( node->pixels, node->pixels, node->w, node->h, desc->blend );

        node->surface = sdl_surface_from_argb32( node->pixels, node->w, node->h );
        if (node->surface == NULL) {
            skin_image_free(node);
            return SKIN_IMAGE_NONE;
        }
    }
    return node;
}


static SkinImageCache   _image_cache[1];
static int              _image_cache_init;

SkinImage*
skin_image_find( SkinImageDesc*  desc )
{
    SkinImageCache*  cache = _image_cache;
    unsigned         hash;
    SkinImage**      pnode = skin_image_lookup_p( cache, desc, &hash );
    SkinImage*       node  = *pnode;

    if (!_image_cache_init) {
        _image_cache_init = 1;
        skin_image_cache_init(cache);
    }

    if (node) {
        node->ref_count += 1;
        return skin_image_cache_raise( cache, node );
    }
    node = skin_image_create( desc, hash );
    if (node == SKIN_IMAGE_NONE)
        return node;

    /* add to hash table */
    node->link = *pnode;
    *pnode     = node;

    /* add to mru list */
    skin_image_cache_raise( cache, node );

    D( "skin_image_cache: add '%s' (rot=%d), %d pixels\n",
       node->desc.path, node->desc.rotation, node->w*node->h );

    cache->total_pixels += node->w*node->h;
    if (cache->total_pixels > cache->max_pixels)
        skin_image_cache_flush( cache );

    return node;
}


SkinImage*
skin_image_find_simple( const char*  path )
{
    SkinImageDesc  desc;

    desc.path     = path;
    desc.rotation = SKIN_ROTATION_0;
    desc.blend    = SKIN_BLEND_FULL;

    return skin_image_find( &desc );
}


SkinImage*
skin_image_ref( SkinImage*  image )
{
    if (image && image != _no_image)
        image->ref_count += 1;

    return image;
}


void
skin_image_unref( SkinImage**  pimage )
{
    SkinImage*  image = *pimage;

    if (image) {
        if (image != _no_image && --image->ref_count == 0) {
            if ((image->flags & SKIN_IMAGE_CLONE) != 0) {
                skin_image_free(image);
            }
        }
        *pimage = NULL;
    }
}


SkinImage*
skin_image_rotate( SkinImage*  source, SkinRotation  rotation )
{
    SkinImageDesc  desc;
    SkinImage*     image;

    if (source == _no_image || source->desc.rotation == rotation)
        return source;

    desc          = source->desc;
    desc.rotation = rotation;
    image         = skin_image_find( &desc );
    skin_image_unref( &source );
    return image;
}


SkinImage*
skin_image_clone( SkinImage*  source )
{
    SkinImage*   image;

    if (source == NULL || source == _no_image)
        return SKIN_IMAGE_NONE;

    image = calloc(1,sizeof(*image));
    if (image == NULL)
        goto Fail;

    image->desc  = source->desc;
    image->hash  = source->hash;
    image->flags = SKIN_IMAGE_CLONE;
    image->w     = source->w;
    image->h     = source->h;
    image->pixels = rotate_image( source->pixels, source->w, source->h,
                                  SKIN_ROTATION_0 );
    if (image->pixels == NULL)
        goto Fail;

    image->surface = sdl_surface_from_argb32( image->pixels, image->w, image->h );
    if (image->surface == NULL)
        goto Fail;

    return image;
Fail:
    if (image != NULL)
        skin_image_free(image);
    return SKIN_IMAGE_NONE;
}

SkinImage*
skin_image_clone_full( SkinImage*    source,
                       SkinRotation  rotation,
                       int           blend )
{
    SkinImageDesc   desc;
    SkinImage*      clone;

    if (source == NULL || source == SKIN_IMAGE_NONE)
        return SKIN_IMAGE_NONE;

    if (rotation == SKIN_ROTATION_0 &&
        blend    == SKIN_BLEND_FULL)
    {
        return skin_image_clone(source);
    }

    desc.path     = source->desc.path;
    desc.rotation = rotation;
    desc.blend    = blend;

    clone = skin_image_create( &desc, 0 );
    if (clone != SKIN_IMAGE_NONE)
        clone->flags |= SKIN_IMAGE_CLONE;

    return clone;
}

/* apply blending to a source skin image and copy the result to a target clone image */
extern void
skin_image_blend_clone( SkinImage*  clone, SkinImage*  source, int  blend )
{
    SDL_LockSurface( clone->surface );
    blend_image( clone->pixels, source->pixels, source->w, source->h, blend );
    SDL_UnlockSurface( clone->surface );
    SDL_SetAlpha( clone->surface, SDL_SRCALPHA, 255 );
}

int
skin_image_w( SkinImage*  image )
{
    return  image ? image->w : 0;
}

int
skin_image_h( SkinImage*  image )
{
    return  image ? image->h : 0;
}

int
skin_image_org_w( SkinImage*  image )
{
    if (image) {
        if (image->desc.rotation == SKIN_ROTATION_90 ||
            image->desc.rotation == SKIN_ROTATION_270)
            return image->h;
        else
            return image->w;
    }
    return 0;
}

int
skin_image_org_h( SkinImage*  image )
{
    if (image) {
        if (image->desc.rotation == SKIN_ROTATION_90 ||
            image->desc.rotation == SKIN_ROTATION_270)
            return image->w;
        else
            return image->h;
    }
    return 0;
}

SDL_Surface*
skin_image_surface( SkinImage*  image )
{
    return image ? image->surface : NULL;
}
