#include <cstdio>
#include <cstdlib>
#include <sstream>
using namespace std;

#include "config.h"
#include "keywords.h"
#include "Options.h"
#include "PlanetProperties.h"
#include "xpUtil.h"

#include "DisplayX11.h"
#include "TimerX11.h"

#include "vroot.h"

// The window is static because it needs to stay open between
// renderings.
Window DisplayX11::window;

DisplayX11::DisplayX11(const int tr) : DisplayBase(tr)
{
    Options *options = Options::getInstance();

    if (options->DisplayMode() == WINDOW)
        display = TimerX11::DisplayID();
    else
        display = XOpenDisplay(NULL);

    if (display == NULL)
        xpExit("Can't open X display\n", __FILE__, __LINE__);

    const int screen_num    = DefaultScreen(display);

    if (options->VirtualRoot())
        root = VirtualRootWindowOfScreen(ScreenOfDisplay(display, screen_num));
    else
        root = ScreenOfDisplay(display, screen_num)->root;
    
    XWindowAttributes xgwa;
    XGetWindowAttributes(display, root, &xgwa);
    fullWidth_  = xgwa.width;
    fullHeight_ = xgwa.height;

    switch (options->DisplayMode())
    {
    case WINDOW:
    {
        if (options->XID())
        {
            window = static_cast<Window> (options->XID());
        }
        else
        {
            width_ = options->getWidth();
            height_ = options->getHeight();
            
            if (times_run == 0)
            {
                int x = options->getWindowX();
                int y = options->getWindowY();
                if (options->GeometryMask() & XNegative) 
                    x += (fullWidth_ - width_);
                if (options->GeometryMask() & YNegative) 
                    y += (fullHeight_ - height_);
            
                window = XCreateSimpleWindow(display, root, x, y, 
                                             width_, height_, 4,
                                             WhitePixel(display, screen_num),
                                             BlackPixel(display, screen_num));
            
                if (options->GeometryMask() != NoValue)
                {
                    XSizeHints *hints = XAllocSizeHints();
                    hints->flags = USPosition;
                    XSetWMNormalHints(display, window, hints);
                }

                string title;
                if (options->WindowTitle().empty()) 
                {
                    title.assign("Xplanet ");
                    title += VERSION;
                }
                else
                {
                    title.assign(options->WindowTitle());
                }

                XTextProperty windowName;
                char *titlec = (char *) title.c_str();
                XStringListToTextProperty(&titlec, 1, &windowName);
                XSetWMName(display, window, &windowName);       
            }
        }
        XGetWindowAttributes(display, window, &xgwa);
        width_  = xgwa.width;
        height_ = xgwa.height;
    }
    break;
    case ROOT:
        if (options->GeometrySelected())
        {
            width_ = options->getWidth();
            height_ = options->getHeight();
        }
        else
        {
            width_ = fullWidth_;
            height_ = fullHeight_;
        }
        
        window = root;
        break;
    default:
        xpExit("DisplayX11: Unknown display mode?\n", __FILE__, __LINE__);
    }

    if (!options->CenterSelected())
    {
        if (width_ % 2 == 0)
            options->CenterX(width_/2 - 0.5);
        else
            options->CenterX(width_/2);

        if (height_ % 2 == 0)
            options->CenterY(height_/2 - 0.5);
        else
            options->CenterY(height_/2);
    }

    allocateRGBData();
}

DisplayX11::~DisplayX11()
{
}

void
DisplayX11::renderImage(PlanetProperties *planetProperties[])
{
    drawLabel(planetProperties);

    Options *options = Options::getInstance();

    Pixmap pixmap;
        
    switch (options->DisplayMode())
    {
    case WINDOW:
        pixmap = createPixmap(rgb_data, width_, height_);
        XMapWindow(display, window);
        break;
    case ROOT:
        if (options->GeometrySelected()) PlaceImageOnRoot();

        pixmap = createPixmap(rgb_data, fullWidth_, fullHeight_);

        if (options->Transparency())
        {
            // Set the background pixmap for Eterms and aterms.  This
            // code is taken from the Esetroot source.
            Atom prop_root, prop_esetroot, type;
            int format;
            unsigned long length, after;
            unsigned char *data_root, *data_esetroot;
            
            prop_root = XInternAtom(display, "_XROOTPMAP_ID", True);
            prop_esetroot = XInternAtom(display, "ESETROOT_PMAP_ID", True);
            
            if (prop_root != None && prop_esetroot != None) 
            {
                XGetWindowProperty(display, root, prop_root, 
                                   0L, 1L, False, AnyPropertyType,
                                   &type, &format, &length, 
                                   &after, &data_root);
                if (type == XA_PIXMAP) 
                {
                    XGetWindowProperty(display, root, prop_esetroot, 
                                       0L, 1L, False, AnyPropertyType,
                                       &type, &format, &length, 
                                       &after, &data_esetroot);
                    if (data_root && data_esetroot) 
                    {
                        if (type == XA_PIXMAP 
                            && *((Pixmap *) data_root) == *((Pixmap *) data_esetroot)) 
                        {
                            XKillClient(display, *((Pixmap *) data_root));
                        }
                    }
                }
            }
            /* This will locate the property, creating it if it
             * doesn't exist */
            prop_root = XInternAtom(display, "_XROOTPMAP_ID", False);
            prop_esetroot = XInternAtom(display, "ESETROOT_PMAP_ID", False);
            
            /* The call above should have created it.  If that failed,
             * we can't continue. */
            if (prop_root == None || prop_esetroot == None) 
            {
                xpWarn("Can't set pixmap for transparency\n", 
                       __FILE__, __LINE__);
            }
            else
            {
                XChangeProperty(display, root, prop_root, XA_PIXMAP, 
                                32, PropModeReplace,
                                (unsigned char *) &pixmap, 1);
                XChangeProperty(display, root, prop_esetroot, XA_PIXMAP,
                                32, PropModeReplace,
                                (unsigned char *) &pixmap, 1);
                XSetCloseDownMode(display, RetainPermanent);
                XFlush(display);
            }
        }
        break;
    default:
        xpExit("Unknown X11 display mode?\n", __FILE__, __LINE__);
    }

    XSetWindowBackgroundPixmap(display, window, pixmap);
    if (!options->Transparency()) XFreePixmap(display, pixmap);
    XClearWindow(display, window);
    XFlush(display);

    if (options->DisplayMode() == ROOT) XCloseDisplay(display);
}

void
DisplayX11::computeShift(unsigned long mask, 
                         unsigned char &left_shift, 
                         unsigned char &right_shift)
{
    left_shift = 0;
    right_shift = 8;
    if (mask != 0)
    {
        while ((mask & 0x01) == 0)
        {
            left_shift++;
            mask >>= 1;
        }
        while ((mask & 0x01) == 1)
        {
            right_shift--;
            mask >>= 1;
        }
    }
}

Pixmap
DisplayX11::createPixmap(const unsigned char *rgb, 
                         const int pixmap_width, const int pixmap_height)
{
    int i, j;   // loop variables 

    const int screen_num = DefaultScreen(display);

    const int depth = DefaultDepth(display, screen_num);
    Visual *visual = DefaultVisual(display, screen_num);
    Colormap colormap = DefaultColormap(display, screen_num);

    Pixmap tmp = XCreatePixmap(display, window, pixmap_width, pixmap_height, 
                               depth);

    char *pixmap_data = NULL;
    switch (depth)
    {
    case 32:
    case 24:
        pixmap_data = new char[4 * pixmap_width * pixmap_height];
        break;
    case 16:
    case 15:
        pixmap_data = new char[2 * pixmap_width * pixmap_height];
        break;
    case 8:
        pixmap_data = new char[pixmap_width * pixmap_height];
        break;
    default:
        break;
    }

    XImage *ximage = XCreateImage(display, visual, depth, ZPixmap, 0,
                                  pixmap_data, pixmap_width, pixmap_height, 
                                  8, 0);

    int entries;
    XVisualInfo v_template;
    v_template.visualid = XVisualIDFromVisual(visual);
    XVisualInfo *visual_info = XGetVisualInfo(display, VisualIDMask, 
                                              &v_template, &entries);

    unsigned long ipos = 0;
    switch (visual_info->c_class)
    {
    case PseudoColor:  
    {
        XColor xc;
        xc.flags = DoRed | DoGreen | DoBlue;

        int num_colors = 256;
        XColor *colors = new XColor[num_colors];
        for (i = 0; i < num_colors; i++) colors[i].pixel = (unsigned long) i;
        XQueryColors(display, colormap, colors, num_colors);
        
        int *closest_color = new int[num_colors];

        for (i = 0; i < num_colors; i++)
        {
            xc.red = (i & 0xe0) << 8;           // highest 3 bits
            xc.green = (i & 0x1c) << 11;        // middle 3 bits
            xc.blue = (i & 0x03) << 14;         // lowest 2 bits

            // find the closest color in the colormap
            double distance, distance_squared, min_distance = 0;
            for (int ii = 0; ii < num_colors; ii++)
            {
                distance = colors[ii].red - xc.red;
                distance_squared = distance * distance;
                distance = colors[ii].green - xc.green;
                distance_squared += distance * distance;
                distance = colors[ii].blue - xc.blue;
                distance_squared += distance * distance;
                
                if ((ii == 0) || (distance_squared <= min_distance))
                {
                    min_distance = distance_squared;
                    closest_color[i] = ii;
                }
            }
        }

        for (j = 0; j < pixmap_height; j++)
        {
            for (i = 0; i < pixmap_width; i++)
            {
                xc.red = (unsigned short) (rgb[ipos++] & 0xe0);
                xc.green = (unsigned short) (rgb[ipos++] & 0xe0);
                xc.blue = (unsigned short) (rgb[ipos++] & 0xc0);

                xc.pixel = xc.red | (xc.green >> 3) | (xc.blue >> 6);
                XPutPixel(ximage, i, j, 
                          colors[closest_color[xc.pixel]].pixel);
            }
        }
        delete [] colors;
        delete [] closest_color;
    }
    break;
    case TrueColor:
    {
        unsigned char red_left_shift;
        unsigned char red_right_shift;
        unsigned char green_left_shift;
        unsigned char green_right_shift;
        unsigned char blue_left_shift;
        unsigned char blue_right_shift;

        computeShift(visual_info->red_mask, red_left_shift, 
                     red_right_shift);
        computeShift(visual_info->green_mask, green_left_shift, 
                     green_right_shift);
        computeShift(visual_info->blue_mask, blue_left_shift, 
                     blue_right_shift);

        unsigned long pixel;
        unsigned long red, green, blue;
        for (j = 0; j < pixmap_height; j++)
        {
            for (i = 0; i < pixmap_width; i++)
            {
                red = (unsigned long) 
                    rgb[ipos++] >> red_right_shift;
                green = (unsigned long) 
                    rgb[ipos++] >> green_right_shift;
                blue = (unsigned long) 
                    rgb[ipos++] >> blue_right_shift;
                
                pixel = (((red << red_left_shift) & visual_info->red_mask)
                         | ((green << green_left_shift) 
                            & visual_info->green_mask)
                         | ((blue << blue_left_shift) 
                            & visual_info->blue_mask));

                XPutPixel(ximage, i, j, pixel);
            }
        }
    }
    break;
    default:
    {
        ostringstream errStr;
        errStr << "createPixmap: visual = " << visual_info->c_class << endl
               << "Visual should be either PseudoColor or TrueColor\n";
        xpWarn(errStr.str(), __FILE__, __LINE__);
        return(tmp);
    }
    }
    
    GC gc = XCreateGC(display, window, 0, NULL);
    XPutImage(display, tmp, gc, ximage, 0, 0, 0, 0, pixmap_width, 
              pixmap_height);

    XFreeGC(display, gc);

    XFree(visual_info);

    delete [] pixmap_data;

    // Set ximage data to NULL since pixmap data was deallocated above
    ximage->data = NULL;
    XDestroyImage(ximage);

    return(tmp);
}

void
DisplayX11::decomposePixmap(const Pixmap p, unsigned char *rgb)
{
    int i, j;
    unsigned long ipos = 0;

    const int screen_num = DefaultScreen(display);
    Visual *visual = DefaultVisual(display, screen_num);

    int entries;
    XVisualInfo v_template;
    v_template.visualid = XVisualIDFromVisual(visual);
    XVisualInfo *visual_info = XGetVisualInfo(display, VisualIDMask,
                                              &v_template, &entries);

    Colormap colormap = DefaultColormap(display, screen_num);

    XImage *ximage = XGetImage(display, p, 0, 0, width_, 
                               height_, AllPlanes, ZPixmap);

    switch (visual_info->c_class)
    {
    case PseudoColor:
    {
        XColor *xc = new XColor[width_];

        for (j = 0; j < height_; j++)
        {
            for (i = 0; i < width_; i++)
                xc[i].pixel = XGetPixel(ximage, i, j);

            XQueryColors(display, colormap, xc, width_);

            for (i = 0; i < width_; i++)
            {
                rgb[ipos++] = (unsigned char) (xc[i].red >> 8);
                rgb[ipos++] = (unsigned char) (xc[i].green >> 8);
                rgb[ipos++] = (unsigned char) (xc[i].blue >> 8);
            }
        }
        delete [] xc;
    }
    break;
    case TrueColor:
    {
        unsigned char red_left_shift;
        unsigned char red_right_shift;
        unsigned char green_left_shift;
        unsigned char green_right_shift;
        unsigned char blue_left_shift;
        unsigned char blue_right_shift;
        
        computeShift(visual_info->red_mask, red_left_shift, 
                     red_right_shift);
        computeShift(visual_info->green_mask, green_left_shift, 
                     green_right_shift);
        computeShift(visual_info->blue_mask, blue_left_shift, 
                     blue_right_shift);
        
        unsigned long pixel;
        for (j = 0; j < height_; j++)
        {
            for (i = 0; i < width_; i++)
            {
                pixel = XGetPixel(ximage, i, j);
                rgb[ipos++] = ((unsigned char) 
                               (((pixel & visual_info->red_mask) 
                                 >> red_left_shift)
                                << red_right_shift));
                rgb[ipos++] = ((unsigned char) 
                               (((pixel & visual_info->green_mask) 
                                 >> green_left_shift)
                                << green_right_shift));
                rgb[ipos++] = ((unsigned char) 
                               (((pixel & visual_info->blue_mask) 
                                 >> blue_left_shift)
                                << blue_right_shift));
            }
        }
    }
    break;
    default:
    {
        ostringstream errStr;
        errStr << "decomposePixmap: visual = " << visual_info->c_class << endl
               << "Visual should be either PseudoColor or TrueColor\n";
        xpWarn(errStr.str(), __FILE__, __LINE__);
    }
    }

    XFree(visual_info);
    XDestroyImage(ximage);
}
