/*
 * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
 */
#include "common.h"
#include "Graphics2D.h"

#include <math.h>

#include "com_sun_rain_graphics_Graphics2D.h"
#include "com_sun_rain_shapes_Shape.h"

static jclass jFontClass;
static jclass jColorClass;
static jclass jByteBufferClass;
static jclass jImageClass;
static jclass jShapeClass;

Graphics2D::Graphics2D()
{
    m_hwnd = NULL;
    m_hdc = NULL;
    backBuffer = NULL;
    width = height = 0;
}

Graphics2D::~Graphics2D()
{
    if (backBuffer) {
        ::DeleteObject(backBuffer);
        backBuffer = NULL;
    }
}

void Graphics2D::SetWindow(HWND window)
{
    m_hwnd = window;
}

void Graphics2D::begin()
{
    RECT rect;
    if (!::GetClientRect(m_hwnd, &rect)) {
        fprintf(stderr, "Graphics2D::begin() - ::GetClientRect() failed: %x          window: %x       this: %p\n", (int)::GetLastError(), (int)m_hwnd, this);
        return;
    }

    if (!backBuffer ||
            width != (rect.right - rect.left) ||
            height != (rect.bottom - rect.top))
    {
        if (backBuffer) {
            ::DeleteObject(backBuffer);
            backBuffer = NULL;
        }

        width = (rect.right - rect.left);
        height = (rect.bottom - rect.top);

        BITMAPINFOHEADER bmi;

        ::ZeroMemory(&bmi, sizeof(bmi));
        bmi.biSize = sizeof(BITMAPINFOHEADER);
        bmi.biWidth = width;
        bmi.biHeight = -height;
        bmi.biPlanes = 1;
        bmi.biBitCount = 32;
        bmi.biCompression = BI_RGB;

        void * bitmapBitsPtr;//unused

        backBuffer = ::CreateDIBSection(NULL, (BITMAPINFO *) & bmi, DIB_RGB_COLORS,
                &bitmapBitsPtr, NULL, 0);

        //Assume that resizing causes the full view repaint,
        //and thus we don't copy anything to this newly created bitmap.
    }

    m_hdc = ::CreateCompatibleDC(NULL);
    // Note: NULL means the dc exists on the current thread only

    ::SetGraphicsMode(m_hdc, GM_ADVANCED);

    oldBitmap = (HBITMAP)::SelectObject(m_hdc, backBuffer);
    oldBrush = (HBRUSH)::SelectObject(m_hdc, ::GetStockObject(DC_BRUSH));
    oldPen = (HPEN)::SelectObject(m_hdc, ::GetStockObject(DC_PEN));
    oldFont = NULL;
    copyFont = NULL;
}

void Graphics2D::end()
{
    if (m_hdc == NULL) {
        return;
    }

    HDC hdc = ::GetDC(GetWindow());

    ::BitBlt(hdc, 0, 0, width, height, m_hdc, 0, 0, SRCCOPY);
    ::GdiFlush();

    ::SelectObject(m_hdc, oldBitmap);
    ::SelectObject(m_hdc, oldBrush);
    ::SelectObject(m_hdc, oldPen);
    if (oldFont) {
        ::SelectObject(m_hdc, oldFont);
    }
    if (copyFont) {
        ::DeleteObject(copyFont);
    }
    ::DeleteDC(m_hdc);
    ::ReleaseDC(GetWindow(), hdc);
    m_hdc = NULL;
}

void Graphics2D::SelectFont(HFONT hFont)
{
    //NOTE: On Windows XP a font selected in a transformed DC saves some
    //      transformation properties. These properties stay in effect even
    //      after a call to ::RestoreDC(), and even after re-selecting
    //      the same font into the DC afterwards.
    //      To overcome this we make a copy of the font, and select it
    //      instead of the original font.

    // Make a copy of the font
    LOGFONT lFont;
    ::GetObject(hFont, sizeof(lFont), &lFont);
    HFONT cFont = ::CreateFontIndirect(&lFont);

    // Select the copy into the DC
    HFONT oFont = (HFONT)::SelectObject(GetDC(), cFont);

    // Make sure we preserve the value of the original font which is
    // selected in a newly created DC by default so that we can
    // restore it prior to deleting the DC
    if (!oldFont) {
        oldFont = oFont;
    }

    // And make sure we delete the previously created copy (if any)
    if (copyFont) {
        ::DeleteObject(copyFont);
    }

    // Now store the copy handle so that we can delete it later.
    // This happens either here (see above), or in the end() function
    copyFont = cFont;
}

extern "C" {

/*
 * Class:     com_sun_rain_graphics_Graphics2D
 * Method:    _initIDs
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1initIDs
    (JNIEnv *env, jclass jGraphicsClass)
{
    jFontClass = (jclass)env->NewGlobalRef(env->FindClass("com/sun/rain/graphics/Font"));
    jColorClass = (jclass)env->NewGlobalRef(env->FindClass("com/sun/rain/graphics/Color"));
    jByteBufferClass = (jclass)env->NewGlobalRef(env->FindClass("java/nio/ByteBuffer"));
    jImageClass = (jclass)env->NewGlobalRef(env->FindClass("com/sun/rain/graphics/Image"));
    jShapeClass = (jclass)env->NewGlobalRef(env->FindClass("com/sun/rain/shapes/Shape"));
}

/*
 * Class:     com_sun_rain_graphics_Graphics2D
 * Method:    _createGraphics
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_com_sun_rain_graphics_Graphics2D__1createGraphics
    (JNIEnv *env, jobject jGraphics)
{
    Graphics2D *g2d = new Graphics2D();
    return ptr_to_jlong(g2d);
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _setColor
 * Signature: (JLcom/sun/rain/graphics/Color;)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1setColor
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr, jobject jcolor)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::fillRect: ERROR: g2d == NULL!\n");
        return;
    }
    COLORREF color = (COLORREF)jlong_to_ptr(env->GetLongField(jcolor,
                env->GetFieldID(jColorClass, "ptr", "J")));

    ::SetDCBrushColor(g2d->GetDC(), color);
    ::SetDCPenColor(g2d->GetDC(), color);
    ::SetTextColor(g2d->GetDC(), color);
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _setFont
 * Signature: (JLcom/sun/rain/graphics/Font;)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1setFont
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr, jobject jfont)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        return;
    }

    HFONT hFont = (HFONT)jlong_to_ptr(env->GetLongField(jfont, env->GetFieldID(jFontClass, "ptr", "J")));
    if (!hFont) {
        fprintf(stderr, "setFont(): NULL font\n");
        return;
    }

    g2d->SelectFont(hFont);
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _drawImage
 * Signature: (JLcom/sun/rain/graphics/Image;FFFF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1drawImage
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr,
     jobject jimage, jfloat x, jfloat y, jfloat width, jfloat height)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        return;
    }

    HDC hdc = ::CreateCompatibleDC(NULL);
    HBITMAP bmp = (HBITMAP)jlong_to_ptr(env->GetLongField(jimage,
                    env->GetFieldID(jImageClass, "ptr", "J")));
    HBITMAP oldBitmap = (HBITMAP)::SelectObject(hdc, bmp);

    SIZE size;
    size.cx = env->GetIntField(jimage,
            env->GetFieldID(jImageClass, "width", "I"));
    size.cy = env->GetIntField(jimage,
            env->GetFieldID(jImageClass, "height", "I"));

    BLENDFUNCTION bf;
    bf.BlendOp = AC_SRC_OVER;
    bf.BlendFlags = 0;
    bf.SourceConstantAlpha = 0xFF;
    bf.AlphaFormat = AC_SRC_ALPHA;

    ::AlphaBlend(g2d->GetDC(), (int)x, (int)y, (int)width, (int)height,
            hdc, 0, 0, size.cx, size.cy, bf);
    ::GdiFlush();

    ::SelectObject(hdc, oldBitmap);
    ::DeleteDC(hdc);
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _drawString
 * Signature: (JLjava/lang/String;FF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1drawString
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr,
     jstring jString, jfloat x, jfloat y)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::drawString: ERROR: g2d == NULL!\n");
        return;
    }

    int length = env->GetStringLength(jString);
    WCHAR* buffer = new WCHAR[length + 1];
    env->GetStringRegion(jString, 0, length, reinterpret_cast<jchar*>(buffer));
    buffer[length] = L'\0';
    int oldMode = ::SetBkMode(g2d->GetDC(), TRANSPARENT);
    TextOutW(g2d->GetDC(), (int)x, (int)y, buffer, length);
    ::SetBkMode(g2d->GetDC(), oldMode);
    delete[] buffer;
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _fillRect
 * Signature: (JFFFF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1fillRect
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr,
     jfloat x, jfloat y, jfloat width, jfloat height)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::fillRect: ERROR: g2d == NULL!\n");
        return;
    }
    ::Rectangle(g2d->GetDC(), (int)x, (int)y, (int)(x + width), (int)(y + height));
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _fillEllipse
 * Signature: (JFFFF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1fillEllipse
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr,
     jfloat x, jfloat y, jfloat width, jfloat height)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::fillEllipse: ERROR: g2d == NULL!\n");
        return;
    }
    ::Ellipse(g2d->GetDC(), (int)x, (int)y, (int)(x + width), (int)(y + height));
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _fillShape
 * Signature: (JLcom/sun/rain/shapes/Shape;Ljava/nio/ByteBuffer;FF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1fillShape
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr,
     jobject jshape, jobject jbuffer, jfloat x, jfloat y)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        return;
    }

    HRGN rgn = (HRGN)jlong_to_ptr(env->GetLongField(jshape,
                env->GetFieldID(jShapeClass, "ptr", "J")));
    
    if (rgn == NULL)
    {
        int length = (int)env->GetDirectBufferCapacity(jbuffer);
        //XXX: looks like this relies on sizeof(int)==sizeof(float)...
        int *bufferInts = (int*)env->GetDirectBufferAddress(jbuffer);
        float *bufferFloats = (float*)env->GetDirectBufferAddress(jbuffer);
        POINT ctrlpts[3];
        int x1 = 0, y1 = 0;

        ::SaveDC(g2d->GetDC());

        //Set identity transform
        XFORM xf;

        xf.eM11 = 1.f;
        xf.eM12 = 0.f;
        xf.eM21 = 0.f;
        xf.eM22 = 1.f;
        xf.eDx = 0;
        xf.eDy = 0;

        ::SetWorldTransform(g2d->GetDC(), &xf);
        ::MoveToEx(g2d->GetDC(), 0, 0, NULL);

        ::BeginPath(g2d->GetDC());
        
        while (length > 0)
        {
            int type = *bufferInts;
            
            bufferInts += 1;
            bufferFloats += 1;
            length -= 4;
            
            switch (type)
            {
                case com_sun_rain_shapes_Shape_MOVE_TO:
                {
                    ::MoveToEx(g2d->GetDC(), (int)bufferFloats[0], (int)bufferFloats[1], NULL);
                    x1 = (int)bufferFloats[0];
                    y1 = (int)bufferFloats[1];
                    bufferInts += 1;
                    bufferFloats += 1;
                    length -= 4;
                    break;
                }
                case com_sun_rain_shapes_Shape_LINE_TO:
                {
                    ::LineTo(g2d->GetDC(), (int)bufferFloats[0], (int)bufferFloats[1]);
                    x1 = (int)bufferFloats[0];
                    y1 = (int)bufferFloats[1];
                    bufferInts += 1;
                    bufferFloats += 1;
                    length -= 4;
                    break;
                }
                case com_sun_rain_shapes_Shape_QUAD_TO:
                {
                    ctrlpts[0].x = (int) floor(bufferFloats[0]);
                    ctrlpts[0].y = (int) floor(bufferFloats[1]);
                    ctrlpts[2].x = (int) floor(bufferFloats[2]);
                    ctrlpts[2].y = (int) floor(bufferFloats[3]);
                    ctrlpts[1].x = (ctrlpts[0].x * 2 + ctrlpts[2].x) / 3;
                    ctrlpts[1].y = (ctrlpts[0].y * 2 + ctrlpts[2].y) / 3;
                    ctrlpts[0].x = (ctrlpts[0].x * 2 + x1) / 3;
                    ctrlpts[0].y = (ctrlpts[0].y * 2 + y1) / 3;
                    x1 = ctrlpts[2].x;
                    y1 = ctrlpts[2].y;
                    ::PolyBezierTo(g2d->GetDC(), ctrlpts, 3);

                    bufferInts += 2;
                    bufferFloats += 2;
                    length -= 8;
                    break;
                }
            }
        }
        ::CloseFigure(g2d->GetDC());
        ::EndPath(g2d->GetDC());

        rgn = ::PathToRegion(g2d->GetDC());

        ::RestoreDC(g2d->GetDC(), -1);

        //TODO: who's gonna ::DeleteObject() this?
        env->SetLongField(jshape,
                env->GetFieldID(jShapeClass, "ptr", "J"),
                ptr_to_jlong(rgn));
    }

    ::SaveDC(g2d->GetDC());

    //Translate to where the shape to be drawn
    XFORM xf;

    xf.eM11 = 1.f;
    xf.eM12 = 0.f;
    xf.eM21 = 0.f;
    xf.eM22 = 1.f;
    xf.eDx = x;
    xf.eDy = y;

    ::ModifyWorldTransform(g2d->GetDC(), &xf, MWT_LEFTMULTIPLY);
    //TODO: looks like it needs a bit of flip also,
    // but it's unclear how to retreive the height of the shape

    ::PaintRgn(g2d->GetDC(), rgn);

    ::RestoreDC(g2d->GetDC(), -1);
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _translate
 * Signature: (JFF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1translate
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr, jfloat x, jfloat y)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::fillRect: ERROR: g2d == NULL!\n");
        return;
    }

    XFORM xf;

    xf.eM11 = 1.f;
    xf.eM12 = 0.f;
    xf.eM21 = 0.f;
    xf.eM22 = 1.f;
    xf.eDx = x;
    xf.eDy = y;

    ::ModifyWorldTransform(g2d->GetDC(), &xf, MWT_LEFTMULTIPLY);
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _scale
 * Signature: (JFF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1scale
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr, jfloat x, jfloat y)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::fillRect: ERROR: g2d == NULL!\n");
        return;
    }

    XFORM xf;

    xf.eM11 = x;
    xf.eM12 = 0.f;
    xf.eM21 = 0.f;
    xf.eM22 = y;
    xf.eDx = 0.f;
    xf.eDy = 0.f;

    ::ModifyWorldTransform(g2d->GetDC(), &xf, MWT_LEFTMULTIPLY);
}

/*
 * Class:     com_sun_rain_graphics_Graphics2D
 * Method:    _rotate
 * Signature: (JF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1rotate
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr, jfloat a)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::fillRect: ERROR: g2d == NULL!\n");
        return;
    }

    XFORM xf;

#ifndef M_PI
#define M_PI           (3.14159265358979323846)
#endif

#define DEGREES_TO_RADIANS(__ANGLE__) ((__ANGLE__) / 180.0f * (float)M_PI)
    a = -DEGREES_TO_RADIANS(a);

 

    xf.eM11 = cos(a);
    xf.eM12 = sin(a);
    xf.eM21 = -sin(a);
    xf.eM22 = xf.eM11;
    xf.eDx = 0.f;
    xf.eDy = 0.f;

    ::ModifyWorldTransform(g2d->GetDC(), &xf, MWT_LEFTMULTIPLY);
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _pushCTM
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1pushCTM
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::fillRect: ERROR: g2d == NULL!\n");
        return;
    }

    ::SaveDC(g2d->GetDC());
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _popCTM
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1popCTM
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::fillRect: ERROR: g2d == NULL!\n");
        return;
    }

    ::RestoreDC(g2d->GetDC(), -1);
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _clip
 * Signature: (JFFFF)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1clip
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr,
     jfloat x, jfloat y, jfloat w, jfloat h)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        return;
    }

    ::IntersectClipRect(g2d->GetDC(), (int)x, (int)y, (int)(x + w), (int)(y + h));
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _pushClip
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1pushClip
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr)
{
    //TODO: looks like we want to have a common push/pop that would maintain
    //clip and CTM together.
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        return;
    }

    ::SaveDC(g2d->GetDC());
}

/*
 * Class:     com_sun_rain_shapes_Graphics2D
 * Method:    _popClip
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1popClip
    (JNIEnv *env, jobject jGraphics, jlong graphicsPtr)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        return;
    }

    ::RestoreDC(g2d->GetDC(), -1);
}

/*
 * Class:     com_sun_rain_graphics_Graphics2D
 * Method:    _begin
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1begin
(JNIEnv *env, jobject jGraphics, jlong graphicsPtr, jlong viewPtr)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::begin: ERROR: g2d == NULL!\n");
        return;
    }
    g2d->SetWindow((HWND)viewPtr);
    g2d->begin();
}

/*
 * Class:     com_sun_rain_graphics_Graphics2D
 * Method:    _end
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_com_sun_rain_graphics_Graphics2D__1end
(JNIEnv *env, jobject jGraphics, jlong graphicsPtr)
{
    Graphics2D *g2d = (Graphics2D*)jlong_to_ptr(graphicsPtr);
    if (g2d == NULL) {
        fprintf(stderr, "Graphics2D::end: ERROR: g2d == NULL!\n");
        return;
    }
    g2d->end();
}

} // extern "C"
