//
// PImage.cc for pekwm
// Copyright © 2005-2015 the pekwm development team
//
// This program is licensed under the GNU GPL.
// See the LICENSE file for more information.
//

#include "config.h"

#include "PImage.hh"
#include "x11.hh"
#include "Util.hh"

#include <iostream>

extern "C" {
#include <X11/Xutil.h>
}

using std::cerr;
using std::endl;
using std::string;

vector<PImageLoader*> PImage::_loaders;

/**
 * PImage constructor, loads image if one is specified.
 *
 * @param dpy Display image is valid on.
 * @param path Path to image file, if specified this is loaded.
 */
PImage::PImage(const std::string &path)
    : _type(IMAGE_TYPE_NO), _pixmap(None), _mask(None), _width(0), _height(0),
      _data(0), _has_alpha(false), _use_alpha(false)
{
    if (path.size()) {
        if (! load(path)) {
            throw LoadException(path.c_str());
        }
    }
}

//! @brief PImage destructor.
PImage::~PImage(void)
{
    unload();
}

//! @brief Loads image from file.
//! @param file File to load.
//! @return Returns true on success, else false.
bool
PImage::load(const std::string &file)
{
    string ext(Util::getFileExt(file));
    if (! ext.size()) {
        cerr << " *** WARNING: no file extension on " << file << "!" << endl;
        return false;
    }

    vector<PImageLoader*>::const_iterator it(_loaders.begin());
    for (; it != _loaders.end(); ++it) {
        if (! strcasecmp((*it)->getExt(), ext.c_str())) {
            _data = (*it)->load(file, _width, _height, _has_alpha, _use_alpha);
            if (_data) {
                _pixmap = createPixmap(_data, _width, _height);
                _mask = createMask(_data, _width, _height);
                break;
            }
        }
    }

    return (_data);
}

//! @brief Frees resources used by image.
void
PImage::unload(void)
{
    if (_data) {
        delete [] _data;
        _data = 0;
    }
    if (_pixmap) {
        X11::freePixmap(_pixmap);
    }
    if (_mask) {
        X11::freePixmap(_mask);
    }

    _pixmap = None;
    _mask = None;
    _width = 0;
    _height = 0;
}

//! @brief Draws image on drawable.
//! @param draw Drawable to draw on.
//! @param x Destination x coordinate.
//! @param y Destination y coordinate.
//! @param width Destination width, defaults to 0 which expands to image size.
//! @param height Destination height, defaults to 0 which expands to image size.
void
PImage::draw(Drawable draw, int x, int y, uint width, uint height)
{
    if (! _data) {
        return;
    }

    // Expand variables.
    if (! width) {
        width = _width;
    } if (! height) {
        height = _height;
    }

    // Draw image, select correct drawing method depending on image type,
    // size and if alpha exists.
    if ((_type == IMAGE_TYPE_FIXED)
        || ((_type == IMAGE_TYPE_SCALED)
            && (_width == width) && (_height == height))) {
        if (_use_alpha) {
            drawAlphaFixed(draw, x, y, width, height);
        } else {
            drawFixed(draw, x, y, width, height);
        }
    } else if (_type == IMAGE_TYPE_SCALED) {
        if (_use_alpha) {
            drawAlphaScaled(draw, x, y, width, height);
        } else {
            drawScaled(draw, x, y, width, height);
        }
    } else if (_type == IMAGE_TYPE_TILED) {
        if (_use_alpha) {
            drawAlphaTiled(draw, x, y, width, height);
        } else {
            drawTiled(draw, x, y, width, height);
        }
    }
}

//! @brief Returns pixmap at sizen.
//! @param need_free Is set to wheter shape mask returned needs to be freed.
//! @param width Pixmap width, defaults to 0 which expands to image size.
//! @param height Pixmap height, defaults to 0 which expands to image size.
//! @return Returns pixmap at size or None on error.
Pixmap
PImage::getPixmap(bool &need_free, uint width, uint height)
{
    // Default
    Pixmap pix = None;
    need_free = false;

    // Expand parameters.
    if (! width) {
        width = _width;
    }
    
    if (! height) {
        height = _height;
    }
    
    // Same size, return _pixmap.
    if ((width == _width) && (height == _height)) {
        pix = _pixmap;
    } else {
        uchar *scaled_data;

        scaled_data = getScaledData(width, height);
        if (scaled_data) {
            need_free = true;
            pix = createPixmap(scaled_data, width, height);
            delete [] scaled_data;
        }
    }

    return pix;
}

//! @brief Returns shape mask at size, if any.
//! @param need_free Is set to wheter shape mask returned needs to be freed.
//! @param width Shape mask width, defaults to 0 which expands to image size.
//! @param height Shape mask height, defaults to 0 which expands to image size.
//! @return Returns shape mask at size or None if there is no mask information.
Pixmap
PImage::getMask(bool &need_free, uint width, uint height)
{
    Pixmap pix = None;
    need_free = false;

    if (! _has_alpha || ! _use_alpha) {
        return None;
    }

    // Expand parameters.
    if (! width) {
        width = _width;
    }
    
    if (! height) {
        height = _height;
    }
    
    // Same size, return _mask.
    if ((width == _width) && (height == _height)) {
        pix = _mask;
    } else {
        uchar *scaled_data;

        scaled_data = getScaledData(width, height);
        if (scaled_data) {
            need_free = true;
            pix = createMask(scaled_data, width, height);
            delete [] scaled_data;
        }
    }

    return pix;
}

//! @brief Scales image to size.
//! @param width Width to scale image to.
//! @param height Height to scale image to.
void
PImage::scale(uint width, uint height)
{
    // Invalid width or height or no need to scale.
    if (! width || ! height || ((width == _width) && (height == _height))) {
        return;
    }
    uchar *scaled_data;

    scaled_data = getScaledData(width, height);
    if (scaled_data) {
        // Free old resources.
        unload();

        // Set data pointer and create pixmap and mask at new size.
        _data = scaled_data;
        _pixmap = createPixmap(_data, width, height);
        _mask = createMask(_data, width, height);
        _width = width;
        _height = height;
    }
}

//! @brief Draw image at position, not scaling.
void
PImage::drawFixed(Drawable dest, int x, int y, uint width, uint height)
{
    // Plain copy of the pixmap onto Drawable.
    XCopyArea(X11::getDpy(), _pixmap, dest, X11::getGC(),
              0, 0, width, height, x, y);

}

//! @brief Draw image scaled to fit width and height.
void
PImage::drawScaled(Drawable dest, int x, int y, uint width, uint height)
{
    uchar *scaled_data;
    // Create scaled representation of image.
    scaled_data = getScaledData(width, height);
    if (scaled_data) {
        Pixmap pix;
        // Create pixmap.
        pix = createPixmap(scaled_data, width, height);
        if (pix) {
            XCopyArea(X11::getDpy(), pix, dest, X11::getGC(),
                      0, 0, width, height, x, y);
            X11::freePixmap(pix);
        }

        delete [] scaled_data;
    }
}

//! @brief Draw image tiled to fit width and height.
void
PImage::drawTiled(Drawable dest, int x, int y, uint width, uint height)
{
    // Create a GC with _pixmap as tile and tiled fill style.
    GC gc;
    XGCValues gv;

    gv.fill_style = FillTiled;
    gv.tile = _pixmap;
    gv.ts_x_origin = x;
    gv.ts_y_origin = y;

    gc = XCreateGC(X11::getDpy(), dest,
                   GCFillStyle|GCTile|GCTileStipXOrigin|GCTileStipYOrigin, &gv);

    // Tile the image onto drawable.
    XFillRectangle(X11::getDpy(), dest, gc, x, y, width, height);

    XFreeGC(X11::getDpy(), gc);
}

//! @brief Draw image at position, not scaling.
void
PImage::drawAlphaFixed(Drawable dest, int x, int y, uint width, uint height, uchar *data)
{
    XImage *dest_image = XGetImage(X11::getDpy(), dest,
                                   x, y, width, height, AllPlanes, ZPixmap);
    if (! dest_image) {
        cerr << " *** ERROR: failed to get image for destination." << endl;
        return;
    }

    // Get mask from visual
    Visual *visual = X11::getVisual();
    dest_image->red_mask = visual->red_mask;
    dest_image->green_mask = visual->green_mask;
    dest_image->blue_mask = visual->blue_mask;

    uchar *src;
    uchar r, g, b, a;
    uchar d_r = 0, d_g = 0, d_b = 0;
    float a_percent, a_percent_inv;

    if (data) {
        src = data;
    } else {
        src = _data;
        width = std::min(width, _width);
        height = std::min(height, _height);
    }

    for (uint i_y = 0; i_y < height; ++i_y) {
        for (uint i_x = 0; i_x < width; ++i_x) {
            // Get pixel value, copy them directly if alpha is set to 255.
            r = *src++;
            g = *src++;
            b = *src++;
            a = *src++;

            // Alpha not 100% solid, blend
            if (a != 255) {
                // Get RGB values from pixel.
                getRgbFromPixel(dest_image, XGetPixel(dest_image, i_x, i_y),
                                d_r, d_g, d_b);

                a_percent = static_cast<float>(a) / 255;
                a_percent_inv = 1 - a_percent;

                r = static_cast<uchar>((a_percent_inv * d_r) + (a_percent * r));
                g = static_cast<uchar>((a_percent_inv * d_g) + (a_percent * g));
                b = static_cast<uchar>((a_percent_inv * d_b) + (a_percent * b));
            }

            XPutPixel(dest_image, i_x, i_y, getPixelFromRgb(dest_image, r, g, b));
        }
    }

    XPutImage(X11::getDpy(), dest, X11::getGC(), dest_image,
              0, 0, x, y, width, height);
    XDestroyImage(dest_image);
}

//! @brief Draw image scaled to fit width and height.
void
PImage::drawAlphaScaled(Drawable dest, int x, int y, uint width, uint height)
{
    uchar *scaled_data = getScaledData(width, height);
    if (scaled_data) {
        drawAlphaFixed(dest, x, y, width, height, scaled_data);
        delete [] scaled_data;
    }
}

//! @brief Draw image tiled to fit width and height.
void
PImage::drawAlphaTiled(Drawable dest, int x, int y, uint width, uint height)
{
    // FIXME: Implement tiled rendering with alpha support
    drawTiled(dest, x, y, width, height);
}

//! @brief Creates Pixmap from data.
//! @param data Pointer to data to create pixmap from.
//! @param width Width of image data is representing.
//! @param height Height of image data is representing.
//! @return Returns Pixmap on success, else None.
Pixmap
PImage::createPixmap(uchar *data, uint width, uint height)
{
    XImage *ximage;
    Pixmap pix = None;

    ximage = createXImage(data, width, height);
    if (ximage) {
        pix = X11::createPixmap(width, height);
        XPutImage(X11::getDpy(), pix, X11::getGC(), ximage,
                  0, 0, 0, 0, width, height);

        delete [] ximage->data;
        ximage->data = 0;
        XDestroyImage(ximage);
    }

    return pix;
}

//! @brief Creates shape mask Pixmap from data.
//! @param data Pointer to data to create mask from.
//! @param width Width of image data is representing.
//! @param height Height of image data is representing.
//! @return Returns Pixmap mask on success, else None.
Pixmap
PImage::createMask(uchar *data, uint width, uint height)
{
    if (! _has_alpha || ! _use_alpha) {
        return None;
    }

    // Create XImage
    XImage *ximage;
    ximage = XCreateImage(X11::getDpy(), X11::getVisual(),
                          1, ZPixmap, 0, 0, width, height, 32, 0);
    if (! ximage) {
        cerr << " *** WARNING: unable to create XImage!" << endl;
        return None;
    }

    // Alocate ximage data storage.
    ximage->data = new char[ximage->bytes_per_line * height / sizeof(char)];

    uchar *src = data + 3; // Skip R, G and B.
    ulong pixel_trans, pixel_solid;

    pixel_trans = X11::getBlackPixel();
    pixel_solid = X11::getWhitePixel();

    for (uint y = 0; y < height; ++y) {
        for (uint x = 0; x < width; ++x) {
            XPutPixel(ximage, x, y, (*src > 127) ? pixel_solid : pixel_trans);
            src += 4; // Skip R, G, B and A
        }
    }

    Pixmap pix{X11::createPixmapMask(width, height)};
    GC gc = XCreateGC(X11::getDpy(), pix, 0, 0);
    XPutImage(X11::getDpy(), pix, gc, ximage, 0, 0, 0, 0, width, height);
    XFreeGC(X11::getDpy(), gc);

    delete [] ximage->data;
    ximage->data = 0;
    XDestroyImage(ximage);

    return pix;
}

//! @brief Createx XImage from data.
//! @param data Pointer to data to create XImage from.
//! @param width Width of image data is representing.
//! @param height Height of image data is representing.
XImage*
PImage::createXImage(uchar *data, uint width, uint height)
{
    // Create XImage
    XImage *ximage;
    ximage = XCreateImage(X11::getDpy(), X11::getVisual(),
                          X11::getDepth(), ZPixmap, 0, 0,
                          width, height, 32, 0);
    if (! ximage) {
        cerr << " *** WARNING: unable to create XImage!" << endl;
        return 0;
    }

    // Allocate ximage data storage.
    ximage->data = new char[ximage->bytes_per_line * height / sizeof(char)];

    uchar *src = data;
    uchar r, g, b;

    // Put data into XImage.
    for (uint y = 0; y < height; ++y) {
        for (uint x = 0; x < width; ++x) {
            r = *src++;
            g = *src++;
            b = *src++;
            if (_has_alpha) {
                src++;
            }
            
            XPutPixel(ximage, x, y, getPixelFromRgb(ximage, r, g, b));
        }
    }

    return ximage;
}

//! @brief Scales image data and returns pointer to new data.
//! @param width Width of image data to return.
//! @param height Height of image data to return.
//! @return Pointer to image data on success, else 0.
//! @todo Implement decent scaling routine with data [] cache?
uchar*
PImage::getScaledData(uint width, uint height)
{
    if (! width || ! height) {
        return 0;
    }
    
    // Calculate aspect ratio.
    float ratio_x, ratio_y;
    ratio_x = static_cast<float>(_width) / static_cast<float>(width);
    ratio_y = static_cast<float>(_height) / static_cast<float>(height);

    // Allocate memory.
    uchar *scaled_data, *dest;
    scaled_data = new uchar[width * height * (_has_alpha ? 4 : 3)];
    dest = scaled_data;

    // Scale image.
    int i_src;
    float f_src;
    for (uint y = 0; y < height; ++y) {
        f_src = static_cast<int>(ratio_y * y) * _width;
        for (uint x = 0; x < width; ++x) {
            i_src = static_cast<int>(f_src);
            i_src *= (_has_alpha ? 4 : 3);

            *dest++ = _data[i_src++];
            *dest++ = _data[i_src++];
            *dest++ = _data[i_src++];
            if (_has_alpha) {
                *dest++ = _data[i_src++];
            }

            f_src += ratio_x;
        }
    }

    return scaled_data;
}
