/*
 * File Name: image_page.cpp
 */

/*
 * This file is part of uds-plugin-images.
 *
 * uds-plugin-images is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * uds-plugin-images 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Copyright (C) 2008 iRex Technologies B.V.
 * All rights reserved.
 */

#include <string.h>
#ifdef WIN32
#include <windows.h>
#else
#include <sys/sysinfo.h>
#endif
#include <time.h>

#include <cassert>
#include <gdk-pixbuf/gdk-pixbuf-transform.h>

#include "plugin_config.h"

#include "image_page.h"
#include "image_dither.h"
#include "utils.h"
#include "log.h"

namespace images
{

bool ImagePage::g_type_initialize;

ImagePage::ImagePage(const std::string & p, 
                     const ImagePageAttrs & request)
: path(p)
, in_use(false)
, timestamp(0)
, loader(0)
, valid(false)
{
    LOGPRINTF("%s", p.c_str());
    
    attrs.desired_width = request.desired_width;
    attrs.desired_height = request.desired_height;
    attrs.desired_color_depth = request.desired_color_depth;

    attrs.zoom = request.zoom;
    attrs.rotation = request.rotation;
    attrs.final_rotation = request.final_rotation;

    display_width = request.desired_width;
    display_height = request.desired_height;
}

ImagePage::~ImagePage(void)
{
    LOGPRINTF("entry %s", path.c_str());

    destroy();
}

void ImagePage::init_type_system()
{
    LOGPRINTF("entry");

    if (!g_type_initialize)
    {
        g_type_initialize = true;
        // Since UDS already calls g_type_init, no need do it again here.
        // Removing it because valgrind reports g_type_init causes memory leak.
        // This might avoid leaking in plugin side.
        // g_type_init();
    }
}

// Steps to render a image
// 1. Load the image. 
// 3. Dither the image.
bool ImagePage::render(void)
{
    LOGPRINTF("entry");
/*
    if (!try_to_load())
    {
        return false;
    }

    if (!try_to_rotate())
    {
        return false;
    }

    if (!try_to_scale())
    {
        return false;
    }
*/
    if (!try_to_load_at_scale())
    {
        return false;
    }

    if (!try_to_rotate())
    {
        return false;
    }

    if (!update_attributes())
    {
        return false;
    }

    if (!try_to_dither())
    {
        return false;
    }

    return true;
}

/*
bool ImagePage::try_to_load(void)
{
    LOGPRINTF("entry");

    if (loader)
    {
        return true;
    }

    // Make sure type system has been initialized in order to 
    // use gdk-pixbuf.
    init_type_system();

    // Use gdk-pixbuf to load the image.
    loader = gdk_pixbuf_new_from_file(path.c_str(), 0);
    assert(loader);
    if (loader)
    {
        attrs.original_width  = gdk_pixbuf_get_width(loader);
        attrs.original_height = gdk_pixbuf_get_height(loader);
        attrs.original_color_depth = gdk_pixbuf_get_bits_per_sample(loader)
                                     * gdk_pixbuf_get_n_channels(loader);
        return true;
    }
    return false;
}
*/
    
void ImagePage::calc_desired_dimension(ImagePageAttrs & request)
{
    int w = request.original_width;
    int h = request.original_height;
    unsigned int display_w = request.desired_width;
    unsigned int display_h = request.desired_height;
    
    PluginRotationDegree autorotate = check_autorotate(w, h, display_w, display_h);

    // Correct for autorotation, if needed.
    int rotation = static_cast<int>(request.rotation);
    rotation = (rotation + autorotate) % Clockwise_Degrees_360;
    request.final_rotation = static_cast<PluginRotationDegree>(rotation);
    if (   rotation == Clockwise_Degrees_90
        || rotation == Clockwise_Degrees_270)
    {
        w = request.original_height;
        h = request.original_width;
        request.original_width = w;
        request.original_height = h;
    }

    // Calculate the desired width and height.
    if (request.zoom == PLUGIN_ZOOM_TO_PAGE ||
        request.zoom == PLUGIN_ZOOM_TO_CROP_BY_PAGE)
    {
        float ratio_width = static_cast<float>(request.desired_width) /
                             static_cast<float>(w);

        float ratio_height = static_cast<float>(request.desired_height) /
                              static_cast<float>(h);

        if (ratio_width > ratio_height)
        {
            request.desired_width = static_cast<int>(ratio_height * w);
        }
        else
        {
            request.desired_height = static_cast<int>(ratio_width * h);
        }
    }
    else if (request.zoom == PLUGIN_ZOOM_TO_WIDTH ||
             request.zoom == PLUGIN_ZOOM_TO_CROP_BY_WIDTH)
    {
        float ratio_width = static_cast<float>(request.desired_width) /
                             static_cast<float>(w);
        request.desired_height = static_cast<int>(h * ratio_width);
    }
    else if (request.zoom > 0)
    {
        request.desired_width = static_cast<int>(
            static_cast<float>(request.zoom * w) / static_cast<float>(100));
        request.desired_height = static_cast<int>(
            static_cast<float>(request.zoom * h) / static_cast<float>(100));
    }
}

/*
bool ImagePage::try_to_scale(void)
{
    LOGPRINTF("entry");

    calc_desired_dimension(attrs);
    
    if ((attrs.desired_width == attrs.original_width)
        && (attrs.desired_height == attrs.original_height))
    {
        return true;
    }
    
    // Rescale it according the original width and height. 
    // Keep the aspect ratio.
    GdkPixbuf * tmp = gdk_pixbuf_scale_simple(loader, 
        attrs.desired_width,
        attrs.desired_height,
        GDK_INTERP_NEAREST);
    assert(tmp);
    if (tmp)
    {
        g_object_unref(loader);
        loader = tmp;
        return true;
    }
    return false;
}
*/
    
bool ImagePage::try_to_load_at_scale(void)
{
    // Check the image is already loaded or not. 
    if (loader)
    {
        return true;
    }
    
    // Make sure type system has been initialized in order to 
    // use gdk-pixbuf.
    init_type_system();
    
    // Get the origial image size.
    gint w, h;
    GdkPixbufFormat * format = gdk_pixbuf_get_file_info(path.c_str(),
                                                        &attrs.original_width, 
                                                        &attrs.original_height);
    if (!format)
    {
        return false;
    }
    
    // Calculate the desired image size and rotation.
    calc_desired_dimension(attrs);
    
    // Use gdk-pixbuf to load the image at size.
    if (attrs.final_rotation == Clockwise_Degrees_90 ||
        attrs.final_rotation == Clockwise_Degrees_270  )
    {
        w = attrs.desired_height;
        h = attrs.desired_width;
    }
    else
    {
        w = attrs.desired_width;
        h = attrs.desired_height;
    }

    loader = gdk_pixbuf_new_from_file_at_size(path.c_str(), w, h, 0);
    assert(loader);

    // Update the attrs.
    if (loader)
    {
        attrs.original_color_depth = gdk_pixbuf_get_bits_per_sample(loader)
                                     * gdk_pixbuf_get_n_channels(loader);
        return true;
    }
    
    return false;    
}

bool ImagePage::try_to_rotate(void)
{
    LOGPRINTF("entry");

    GdkPixbufRotation gdk_rotation = GDK_PIXBUF_ROTATE_NONE;

    // Convert rotation attribute to GDK value.
    switch (attrs.final_rotation)
    {
        case Clockwise_Degrees_0:
        case Clockwise_Degrees_360:
            gdk_rotation = GDK_PIXBUF_ROTATE_NONE;
            break;
        case Clockwise_Degrees_90:
            gdk_rotation = GDK_PIXBUF_ROTATE_CLOCKWISE;
            break;
        case Clockwise_Degrees_180:
            gdk_rotation = GDK_PIXBUF_ROTATE_UPSIDEDOWN;
            break;
        case Clockwise_Degrees_270:
            gdk_rotation = GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE;
            break;
        default:
            ERRORPRINTF("invalid attrs.rotation [%d]", attrs.final_rotation);
            assert(false);
    }

    // Rotate if necessary.
    if (gdk_rotation != GDK_PIXBUF_ROTATE_NONE)
    {
        GdkPixbuf * tmp = gdk_pixbuf_rotate_simple(loader, gdk_rotation);
        assert(tmp);
        if (tmp)
        {
            g_object_unref(loader);
            loader = tmp;
            return true;
        }
        return false;
    }
    else
    {
        LOGPRINTF("No need to rotate.");
        return true;
    }
}

bool ImagePage::try_to_dither(void)
{
    LOGPRINTF("entry");
    
    if (attrs.original_color_depth == attrs.desired_color_depth)
    {
        return true;
    }

    // Currenty, only support dither 32bits or 24bits to 8bits.
    assert(attrs.desired_color_depth == 8);

    using namespace images;
    BitmapAttributes attrs_src, attrs_dst;
    // attrs_src
    attrs_src.width = attrs.desired_width;
    attrs_src.height = attrs.desired_height;
    attrs_src.rowstride = attrs.row_stride;
    attrs_src.data = attrs.data;
    attrs_src.bytes_per_pixel = attrs.original_color_depth / 8;

    ImageDither dither;
    dither.dither_to_8bits(&attrs_src, &attrs_dst);
    
    // update the ImagePageAttrs
    if (attrs.data)
    {
        delete [] attrs.data;
    }
    attrs.data = attrs_dst.data;
    attrs.row_stride = attrs_dst.rowstride;
    
    return true;
}

bool ImagePage::update_attributes(void)
{
    LOGPRINTF("entry");

    // Pixbuf with 24bit or 32bit(alpha).
    if (loader)
    {
        attrs.row_stride     = gdk_pixbuf_get_rowstride(loader);
        attrs.desired_width  = gdk_pixbuf_get_width(loader);
        attrs.desired_height = gdk_pixbuf_get_height(loader);

        // Copy the image data to 'attrs'.
        int nbytes = attrs.row_stride * attrs.desired_height;
        attrs.data = new unsigned char[nbytes];
        assert(attrs.data);
        unsigned char * data = gdk_pixbuf_get_pixels(loader);
        memcpy(attrs.data, data, nbytes);

        // Free the gdk_pixbuf since we have a copy in 'attrs'.
        g_object_unref(loader);
        loader = 0;

        LOGPRINTF("\noriginal_width = %d,"
                   "\noriginal_height = %d,"
                   "\ndesired_width = %d,"
                   "\ndesired_height = %d,"
                   "\nrows_tride = %d,"
                   "\noriginal_color_depth = %d,"
                   "\ndesired_color_depth = %d,"
                   "\nzoom = %f,"
                   "\nrotation = %d,"
                   "\nfinal_rotation = %d,"
                   "\ndata = %p",
                   attrs.original_width,
                   attrs.original_height,
                   attrs.desired_width,
                   attrs.desired_height,
                   attrs.row_stride,
                   attrs.original_color_depth,
                   attrs.desired_color_depth,
                   attrs.zoom,
                   attrs.rotation,
                   attrs.final_rotation,
                   attrs.data);

        return valid = true;
    }

    // It is not a valid image.
    return valid = false;
}

void ImagePage::destroy()
{
    LOGPRINTF("entry");

    path.clear();

    attrs.original_width = 0;
    attrs.original_height = 0;
    attrs.desired_width = 0;
    attrs.desired_height = 0;
    attrs.row_stride = 0;
    attrs.original_color_depth = 0;
    attrs.desired_color_depth = DEFAULT_COLOR_DEPTH;
    attrs.zoom = DEFAULT_ZOOM_FACTOR;
    attrs.rotation = DEFAULT_ROTATION;
    attrs.final_rotation = DEFAULT_ROTATION;
    if (attrs.data)
    {
        delete [] attrs.data;
        attrs.data = 0;
    }

    display_width = 0;
    display_height = 0;
    
    timestamp = 0;
    valid = false;
}

// Define the hash function.
size_t ImagePage::operator()(void) const
{
    return calc_key(path, display_width, display_height, 
                    attrs.zoom, attrs.rotation);
}

size_t ImagePage::calc_key(const std::string & anchor,
                           int width,
                           int height,
                           float zoom,
                           int rotation)
{
    struct PageKey
    {
        char   anchor[MAX_PATH_LEN]; 
        int    display_width;        // display size
        int    display_height;
        float  zoom;
        int    rotation;
    };

    PageKey key;
    int len = sizeof(PageKey);
    memset(&key, 0, len);
    
    memcpy(key.anchor, anchor.c_str(), strlen(anchor.c_str()));
    key.display_width = width;
    key.display_height = height;
    key.zoom = zoom;
    key.rotation = rotation;

    size_t h = make_hash_code((const char*)&key, len);

    LOGPRINTF("return %d", h);

    return h;
}

// Define the equal function.
bool ImagePage::operator == (const ImagePage & right)
{
    LOGPRINTF("entry");

    // Note: Ignore final_rotation for comparison
    if ((path == right.path)
        && (this->attrs.zoom == right.attrs.zoom)
        && (this->attrs.rotation == right.attrs.rotation)
        && (this->display_width == right.display_width)
        && (this->display_height == right.display_height))
    {
        return true;
    }

    return false;
}

// Define operator <
bool ImagePage::operator < (const ImagePage & right)
{
   if (this->in_use && !right.in_use)
   {
       return false;
   }
   else if (!this->in_use && right.in_use)
   {
       return true;
   }
   else
   {
        LOGPRINTF("%ld < %ld ? %d", 
                this->timestamp, right.timestamp, 
                this->timestamp < right.timestamp);

        return (this->timestamp < right.timestamp);
   }
}

// Define operator >
bool ImagePage::operator > (const ImagePage & right)
{
    return !((*this) < right);
}

// Get the approximate size of memory for the page.
int ImagePage::length(void)
{
    int size = attrs.row_stride * attrs.desired_height;
    
    LOGPRINTF("return %d", size);

    return size;
}

int ImagePage::length(const std::string & filename,
                          ImagePageAttrs & request)

{
    calc_desired_dimension(request);

    int size_for_data = request.desired_width *
                        request.desired_height *
                        request.desired_color_depth / 8;

    int size_for_gdk_pixbuf_scale = request.desired_width *
                                    request.desired_height * 4;

/* 
   MvdW: always use the scaled pixbuf size. The original code
   causes OOM error messages even if there is enough memory.

    int size_for_gdk_pixbuf_original = request.original_width *
                                       request.original_height * 4;
*/
    int size_for_gdk_pixbuf = size_for_gdk_pixbuf_scale;
/*        (size_for_gdk_pixbuf_scale > size_for_gdk_pixbuf_original) ?
         size_for_gdk_pixbuf_scale : size_for_gdk_pixbuf_original; */
    
    // Note: this is empirical value!!
    int size = size_for_data + size_for_gdk_pixbuf;
    
    LOGPRINTF("%s \n"
                "original_w=%d, original_h=%d;\n"
                "desired_w=%d, desired_h=%d\n"
                "length=%d", 
                filename.c_str(), 
                request.original_width, request.original_height,
                request.desired_width, request.desired_height,
                size);
    
    return size;
}

void ImagePage::update_timestamp(void)
{
#ifdef WIN32
    timestamp = GetTickCount();
#else
    struct sysinfo s_info;
    sysinfo(&s_info);
    timestamp = s_info.uptime;
#endif
}

// This function calculates how to rotate the image such that it is displayed as large as possible.
//
// Quick fix for tracker 3079: When opening an image it doesn't autorotate to the best fit:
//   Ignore EXIF information from image file.
//   No autorotate when:
//     - image at 100% fits on screen
//     - portrait image on portrait display
//     - landscape image on landscape display
//   Assume device rotation as in DR800 (landscape dipslay is device rotated anti-clockwise).
//   Portrait image on landscape display:
//     - device is rotated anti-clockwise (DR800 mode)
//     - show the image as if device were not rotated
//     - image autorotate clockwise
//   Landscape image on portrait display:
//     - device is not rotated
//     - show the image as if device were rotated anti-clockwise (DR800 mode)
//     - image autorotate anticlockwise
PluginRotationDegree ImagePage::check_autorotate(int w, int h, int display_w, int display_h)
{
    // Image fits on screen: no autorotation.
    if ((w <= display_w) && (h <= display_h))
    {
        return Clockwise_Degrees_0;
    }

    // Portrait image on landscape display: autorotate.
    if ((w < h) && (display_w > display_h))
    {
        return Clockwise_Degrees_270;
    }

    // Landscape image on portrait dislay: autorotate.
    if ((w > h) && (display_w < display_h))
    {
        return Clockwise_Degrees_90;
    }

    return Clockwise_Degrees_0;
}

};


