/*
 * Copyright 2011 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "SkTypes.h"

#if defined(SK_BUILD_FOR_MAC)

#include <AGL/agl.h>

#include <Carbon/Carbon.h>
#include "SkCGUtils.h"

#include "SkWindow.h"
#include "SkCanvas.h"
#include "SkOSMenu.h"
#include "SkTime.h"

#include "SkGraphics.h"
#include <new.h>

static void (*gPrevNewHandler)();

extern "C" {
    static void sk_new_handler()
    {
        if (SkGraphics::SetFontCacheUsed(0))
            return;
        if (gPrevNewHandler)
            gPrevNewHandler();
        else
            sk_throw();
    }
}

static SkOSWindow* gCurrOSWin;
static EventTargetRef gEventTarget;
static EventQueueRef gCurrEventQ;

static OSStatus MyDrawEventHandler(EventHandlerCallRef myHandler,
                                   EventRef event, void *userData) {
    // NOTE: GState is save/restored by the HIView system doing the callback,
    // so the draw handler doesn't need to do it

    OSStatus status = noErr;
    CGContextRef context;
    HIRect        bounds;

    // Get the CGContextRef
    status = GetEventParameter (event, kEventParamCGContextRef,
                                typeCGContextRef, NULL,
                                sizeof (CGContextRef),
                                NULL,
                                &context);

    if (status != noErr) {
        SkDebugf("Got error %d getting the context!\n", status);
        return status;
    }

    // Get the bounding rectangle
    HIViewGetBounds ((HIViewRef) userData, &bounds);

    gCurrOSWin->doPaint(context);
    return status;
}

#define SK_MacEventClass            FOUR_CHAR_CODE('SKec')
#define SK_MacEventKind                FOUR_CHAR_CODE('SKek')
#define SK_MacEventParamName        FOUR_CHAR_CODE('SKev')
#define SK_MacEventSinkIDParamName    FOUR_CHAR_CODE('SKes')

static void set_bindingside(HISideBinding* side, HIViewRef parent, HIBindingKind kind) {
    side->toView = parent;
    side->kind = kind;
    side->offset = 0;
}

static void set_axisscale(HIAxisScale* axis, HIViewRef parent) {
    axis->toView = parent;
    axis->kind = kHILayoutScaleAbsolute;
    axis->ratio = 1;
}

static void set_axisposition(HIAxisPosition* pos, HIViewRef parent, HIPositionKind kind) {
    pos->toView = parent;
    pos->kind = kind;
    pos->offset = 0;
}

SkOSWindow::SkOSWindow(void* hWnd) : fHWND(hWnd), fAGLCtx(NULL)
{
    OSStatus    result;
    WindowRef   wr = (WindowRef)hWnd;

    HIViewRef imageView, parent;
    HIViewRef rootView = HIViewGetRoot(wr);
    HIViewFindByID(rootView, kHIViewWindowContentID, &parent);
    result = HIImageViewCreate(NULL, &imageView);
    SkASSERT(result == noErr);

    result = HIViewAddSubview(parent, imageView);
    SkASSERT(result == noErr);

    fHVIEW = imageView;

    HIViewSetVisible(imageView, true);
    HIViewPlaceInSuperviewAt(imageView, 0, 0);

    if (true) {
        HILayoutInfo layout;
        layout.version = kHILayoutInfoVersionZero;
        set_bindingside(&layout.binding.left, parent, kHILayoutBindLeft);
        set_bindingside(&layout.binding.top, parent, kHILayoutBindTop);
        set_bindingside(&layout.binding.right, parent, kHILayoutBindRight);
        set_bindingside(&layout.binding.bottom, parent, kHILayoutBindBottom);
        set_axisscale(&layout.scale.x, parent);
        set_axisscale(&layout.scale.y, parent);
        set_axisposition(&layout.position.x, parent, kHILayoutPositionLeft);
        set_axisposition(&layout.position.y, rootView, kHILayoutPositionTop);
        HIViewSetLayoutInfo(imageView, &layout);
    }

    HIImageViewSetOpaque(imageView, true);
    HIImageViewSetScaleToFit(imageView, false);

    static const EventTypeSpec  gTypes[] = {
        { kEventClassKeyboard,  kEventRawKeyDown            },
        { kEventClassKeyboard,  kEventRawKeyUp              },
        { kEventClassMouse,        kEventMouseDown                },
        { kEventClassMouse,        kEventMouseDragged            },
        { kEventClassMouse,        kEventMouseMoved            },
        { kEventClassMouse,        kEventMouseUp                },
        { kEventClassTextInput, kEventTextInputUnicodeForKeyEvent   },
        { kEventClassWindow,    kEventWindowBoundsChanged    },
//        { kEventClassWindow,    kEventWindowDrawContent        },
        { SK_MacEventClass,        SK_MacEventKind                }
    };

    EventHandlerUPP handlerUPP = NewEventHandlerUPP(SkOSWindow::EventHandler);
    int                count = SK_ARRAY_COUNT(gTypes);

    result = InstallEventHandler(GetWindowEventTarget(wr), handlerUPP,
                        count, gTypes, this, nil);
    SkASSERT(result == noErr);

    gCurrOSWin = this;
    gCurrEventQ = GetCurrentEventQueue();
    gEventTarget = GetWindowEventTarget(wr);

    static bool gOnce = true;
    if (gOnce) {
        gOnce = false;
        gPrevNewHandler = set_new_handler(sk_new_handler);
    }
}

void SkOSWindow::doPaint(void* ctx)
{
#if 0
    this->update(NULL);

    const SkBitmap& bm = this->getBitmap();
    CGImageRef img = SkCreateCGImageRef(bm);

    if (img) {
        CGRect r = CGRectMake(0, 0, bm.width(), bm.height());

        CGContextRef cg = reinterpret_cast<CGContextRef>(ctx);

        CGContextSaveGState(cg);
        CGContextTranslateCTM(cg, 0, r.size.height);
        CGContextScaleCTM(cg, 1, -1);

        CGContextDrawImage(cg, r, img);

        CGContextRestoreGState(cg);

        CGImageRelease(img);
    }
#endif
}

void SkOSWindow::updateSize()
{
    Rect    r;

    GetWindowBounds((WindowRef)fHWND, kWindowContentRgn, &r);
    this->resize(r.right - r.left, r.bottom - r.top);

#if 0
    HIRect    frame;
    HIViewRef imageView = (HIViewRef)getHVIEW();
    HIViewRef parent = HIViewGetSuperview(imageView);

    HIViewGetBounds(imageView, &frame);
    SkDebugf("------ %d bounds %g %g %g %g\n", r.right - r.left,
             frame.origin.x, frame.origin.y, frame.size.width, frame.size.height);
#endif
}

void SkOSWindow::onHandleInval(const SkIRect& r)
{
    (new SkEvent("inval-imageview", this->getSinkID()))->post();
}

bool SkOSWindow::onEvent(const SkEvent& evt) {
    if (evt.isType("inval-imageview")) {
        this->update(NULL);

        SkEvent query("ignore-window-bitmap");
        if (!this->doQuery(&query) || !query.getFast32()) {
            const SkBitmap& bm = this->getBitmap();

            CGImageRef img = SkCreateCGImageRef(bm);
            HIImageViewSetImage((HIViewRef)getHVIEW(), img);
            CGImageRelease(img);
        }
        return true;
    }
    return INHERITED::onEvent(evt);
}

void SkOSWindow::onSetTitle(const char title[])
{
    CFStringRef str = CFStringCreateWithCString(NULL, title, kCFStringEncodingUTF8);
    SetWindowTitleWithCFString((WindowRef)fHWND, str);
    CFRelease(str);
}

void SkOSWindow::onAddMenu(const SkOSMenu* sk_menu)
{
}

static void getparam(EventRef inEvent, OSType name, OSType type, UInt32 size, void* data)
{
    EventParamType  actualType;
    UInt32            actualSize;
    OSStatus        status;

    status = GetEventParameter(inEvent, name, type, &actualType, size, &actualSize, data);
    SkASSERT(status == noErr);
    SkASSERT(actualType == type);
    SkASSERT(actualSize == size);
}

enum {
    SK_MacReturnKey        = 36,
    SK_MacDeleteKey        = 51,
    SK_MacEndKey        = 119,
    SK_MacLeftKey        = 123,
    SK_MacRightKey        = 124,
    SK_MacDownKey        = 125,
    SK_MacUpKey            = 126,

    SK_Mac0Key          = 0x52,
    SK_Mac1Key          = 0x53,
    SK_Mac2Key          = 0x54,
    SK_Mac3Key          = 0x55,
    SK_Mac4Key          = 0x56,
    SK_Mac5Key          = 0x57,
    SK_Mac6Key          = 0x58,
    SK_Mac7Key          = 0x59,
    SK_Mac8Key          = 0x5b,
    SK_Mac9Key          = 0x5c
};

static SkKey raw2key(UInt32 raw)
{
    static const struct {
        UInt32  fRaw;
        SkKey   fKey;
    } gKeys[] = {
        { SK_MacUpKey,        kUp_SkKey        },
        { SK_MacDownKey,    kDown_SkKey        },
        { SK_MacLeftKey,    kLeft_SkKey        },
        { SK_MacRightKey,   kRight_SkKey    },
        { SK_MacReturnKey,  kOK_SkKey        },
        { SK_MacDeleteKey,  kBack_SkKey        },
        { SK_MacEndKey,        kEnd_SkKey        },
        { SK_Mac0Key,       k0_SkKey        },
        { SK_Mac1Key,       k1_SkKey        },
        { SK_Mac2Key,       k2_SkKey        },
        { SK_Mac3Key,       k3_SkKey        },
        { SK_Mac4Key,       k4_SkKey        },
        { SK_Mac5Key,       k5_SkKey        },
        { SK_Mac6Key,       k6_SkKey        },
        { SK_Mac7Key,       k7_SkKey        },
        { SK_Mac8Key,       k8_SkKey        },
        { SK_Mac9Key,       k9_SkKey        }
    };

    for (unsigned i = 0; i < SK_ARRAY_COUNT(gKeys); i++)
        if (gKeys[i].fRaw == raw)
            return gKeys[i].fKey;
    return kNONE_SkKey;
}

static void post_skmacevent()
{
    EventRef    ref;
    OSStatus    status = CreateEvent(nil, SK_MacEventClass, SK_MacEventKind, 0, 0, &ref);
    SkASSERT(status == noErr);

#if 0
    status = SetEventParameter(ref, SK_MacEventParamName, SK_MacEventParamName, sizeof(evt), &evt);
    SkASSERT(status == noErr);
    status = SetEventParameter(ref, SK_MacEventSinkIDParamName, SK_MacEventSinkIDParamName, sizeof(sinkID), &sinkID);
    SkASSERT(status == noErr);
#endif

    EventTargetRef target = gEventTarget;
    SetEventParameter(ref, kEventParamPostTarget, typeEventTargetRef, sizeof(target), &target);
    SkASSERT(status == noErr);

    status = PostEventToQueue(gCurrEventQ, ref, kEventPriorityStandard);
    SkASSERT(status == noErr);

    ReleaseEvent(ref);
}

pascal OSStatus SkOSWindow::EventHandler( EventHandlerCallRef inHandler, EventRef inEvent, void* userData )
{
    SkOSWindow* win = (SkOSWindow*)userData;
    OSStatus    result = eventNotHandledErr;
    UInt32        wClass = GetEventClass(inEvent);
    UInt32        wKind = GetEventKind(inEvent);

    gCurrOSWin = win;    // will need to be in TLS. Set this so PostEvent will work

    switch (wClass) {
        case kEventClassMouse: {
            Point   pt;
            getparam(inEvent, kEventParamMouseLocation, typeQDPoint, sizeof(pt), &pt);
            SetPortWindowPort((WindowRef)win->getHWND());
            GlobalToLocal(&pt);

            switch (wKind) {
                case kEventMouseDown:
                    if (win->handleClick(pt.h, pt.v, Click::kDown_State)) {
                        result = noErr;
                    }
                    break;
                case kEventMouseMoved:
                    // fall through
                case kEventMouseDragged:
                    (void)win->handleClick(pt.h, pt.v, Click::kMoved_State);
                  //  result = noErr;
                    break;
                case kEventMouseUp:
                    (void)win->handleClick(pt.h, pt.v, Click::kUp_State);
                  //  result = noErr;
                    break;
                default:
                    break;
            }
            break;
        }
        case kEventClassKeyboard:
            if (wKind == kEventRawKeyDown) {
                UInt32  raw;
                getparam(inEvent, kEventParamKeyCode, typeUInt32, sizeof(raw), &raw);
                SkKey key = raw2key(raw);
                if (key != kNONE_SkKey)
                    (void)win->handleKey(key);
            } else if (wKind == kEventRawKeyUp) {
                UInt32 raw;
                getparam(inEvent, kEventParamKeyCode, typeUInt32, sizeof(raw), &raw);
                SkKey key = raw2key(raw);
                if (key != kNONE_SkKey)
                    (void)win->handleKeyUp(key);
            }
            break;
        case kEventClassTextInput:
            if (wKind == kEventTextInputUnicodeForKeyEvent) {
                UInt16  uni;
                getparam(inEvent, kEventParamTextInputSendText, typeUnicodeText, sizeof(uni), &uni);
                win->handleChar(uni);
            }
            break;
        case kEventClassWindow:
            switch (wKind) {
                case kEventWindowBoundsChanged:
                    win->updateSize();
                    break;
                case kEventWindowDrawContent: {
                    CGContextRef cg;
                    result = GetEventParameter(inEvent,
                                               kEventParamCGContextRef,
                                               typeCGContextRef,
                                               NULL,
                                               sizeof (CGContextRef),
                                               NULL,
                                               &cg);
                    if (result != 0) {
                        cg = NULL;
                    }
                    win->doPaint(cg);
                    break;
                }
                default:
                    break;
            }
            break;
        case SK_MacEventClass: {
            SkASSERT(wKind == SK_MacEventKind);
            if (SkEvent::ProcessEvent()) {
                    post_skmacevent();
            }
    #if 0
            SkEvent*        evt;
            SkEventSinkID    sinkID;
            getparam(inEvent, SK_MacEventParamName, SK_MacEventParamName, sizeof(evt), &evt);
            getparam(inEvent, SK_MacEventSinkIDParamName, SK_MacEventSinkIDParamName, sizeof(sinkID), &sinkID);
    #endif
            result = noErr;
            break;
        }
        default:
            break;
    }
    if (result == eventNotHandledErr) {
        result = CallNextEventHandler(inHandler, inEvent);
    }
    return result;
}

///////////////////////////////////////////////////////////////////////////////////////

void SkEvent::SignalNonEmptyQueue()
{
    post_skmacevent();
//    SkDebugf("signal nonempty\n");
}

static TMTask    gTMTaskRec;
static TMTask*    gTMTaskPtr;

static void sk_timer_proc(TMTask* rec)
{
    SkEvent::ServiceQueueTimer();
//    SkDebugf("timer task fired\n");
}

void SkEvent::SignalQueueTimer(SkMSec delay)
{
    if (gTMTaskPtr)
    {
        RemoveTimeTask((QElem*)gTMTaskPtr);
        DisposeTimerUPP(gTMTaskPtr->tmAddr);
        gTMTaskPtr = nil;
    }
    if (delay)
    {
        gTMTaskPtr = &gTMTaskRec;
        memset(gTMTaskPtr, 0, sizeof(gTMTaskRec));
        gTMTaskPtr->tmAddr = NewTimerUPP(sk_timer_proc);
        OSErr err = InstallTimeTask((QElem*)gTMTaskPtr);
//        SkDebugf("installtimetask of %d returned %d\n", delay, err);
        PrimeTimeTask((QElem*)gTMTaskPtr, delay);
    }
}

#define USE_MSAA 0

AGLContext create_gl(WindowRef wref)
{
    GLint major, minor;
    AGLContext ctx;

    aglGetVersion(&major, &minor);
    SkDebugf("---- agl version %d %d\n", major, minor);

    const GLint pixelAttrs[] = {
        AGL_RGBA,
        AGL_STENCIL_SIZE, 8,
#if USE_MSAA
        AGL_SAMPLE_BUFFERS_ARB, 1,
        AGL_MULTISAMPLE,
        AGL_SAMPLES_ARB, 8,
#endif
        AGL_ACCELERATED,
        AGL_DOUBLEBUFFER,
        AGL_NONE
    };
    AGLPixelFormat format = aglChoosePixelFormat(NULL, 0, pixelAttrs);
    //AGLPixelFormat format = aglCreatePixelFormat(pixelAttrs);
    SkDebugf("----- agl format %p\n", format);
    ctx = aglCreateContext(format, NULL);
    SkDebugf("----- agl context %p\n", ctx);
    aglDestroyPixelFormat(format);

    static const GLint interval = 1;
    aglSetInteger(ctx, AGL_SWAP_INTERVAL, &interval);
    aglSetCurrentContext(ctx);
    return ctx;
}

bool SkOSWindow::attach(SkBackEndTypes /* attachType */)
{
    if (NULL == fAGLCtx) {
        fAGLCtx = create_gl((WindowRef)fHWND);
        if (NULL == fAGLCtx) {
            return false;
        }
    }

    GLboolean success = true;

    int width, height;

    success = aglSetWindowRef((AGLContext)fAGLCtx, (WindowRef)fHWND);
    width = this->width();
    height = this->height();

    GLenum err = aglGetError();
    if (err) {
        SkDebugf("---- aglSetWindowRef %d %d %s [%d %d]\n", success, err,
                 aglErrorString(err), width, height);
    }

    if (success) {
        glViewport(0, 0, width, height);
        glClearColor(0, 0, 0, 0);
        glClearStencil(0);
        glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    }
    return success;
}

void SkOSWindow::detach() {
    aglSetWindowRef((AGLContext)fAGLCtx, NULL);
}

void SkOSWindow::present() {
    aglSwapBuffers((AGLContext)fAGLCtx);
}

#endif
