/*
  Simple DirectMedia Layer
  Copyright (C) 1997-2019 Sam Lantinga <slouken@libsdl.org>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/

#ifndef SDL_BWin_h_
#define SDL_BWin_h_

#ifdef __cplusplus
extern "C" {
#endif

#include "../../SDL_internal.h"
#include "SDL.h"
#include "SDL_syswm.h"
#include "SDL_bframebuffer.h"

#ifdef __cplusplus
}
#endif

#include <stdio.h>
#include <AppKit.h>
#include <InterfaceKit.h>
#include <game/DirectWindow.h>
#if SDL_VIDEO_OPENGL
#include <opengl/GLView.h>
#endif
#include "SDL_events.h"
#include "../../main/haiku/SDL_BApp.h"


enum WinCommands {
    BWIN_MOVE_WINDOW,
    BWIN_RESIZE_WINDOW,
    BWIN_SHOW_WINDOW,
    BWIN_HIDE_WINDOW,
    BWIN_MAXIMIZE_WINDOW,
    BWIN_MINIMIZE_WINDOW,
    BWIN_RESTORE_WINDOW,
    BWIN_SET_TITLE,
    BWIN_SET_BORDERED,
    BWIN_SET_RESIZABLE,
    BWIN_FULLSCREEN
};


class SDL_BWin:public BDirectWindow
{
  public:
    /* Constructor/Destructor */
    SDL_BWin(BRect bounds, window_look look, uint32 flags)
        : BDirectWindow(bounds, "Untitled", look, B_NORMAL_WINDOW_FEEL, flags)
    {
        _last_buttons = 0;

#if SDL_VIDEO_OPENGL
        _SDL_GLView = NULL;
        _gl_type = 0;
#endif
        _shown = false;
        _inhibit_resize = false;
        _mouse_focused = false;
        _prev_frame = NULL;

        /* Handle framebuffer stuff */
        _connected = _connection_disabled = false;
        _buffer_created = _buffer_dirty = false;
        _trash_window_buffer = false;
        _buffer_locker = new BLocker();
        _bitmap = NULL;
        _clips = NULL;

#ifdef DRAWTHREAD
        _draw_thread_id = spawn_thread(HAIKU_DrawThread, "drawing_thread",
                            B_NORMAL_PRIORITY, (void*) this);
        resume_thread(_draw_thread_id);
#endif
    }

    virtual ~ SDL_BWin()
    {
        Lock();
        _connection_disabled = true;
        int32 result;

#if SDL_VIDEO_OPENGL
        if (_SDL_GLView) {
            _SDL_GLView->UnlockGL();
            RemoveChild(_SDL_GLView);   /* Why was this outside the if
                                            statement before? */
        }

#endif
        Unlock();
#if SDL_VIDEO_OPENGL
        if (_SDL_GLView) {
            delete _SDL_GLView;
        }
#endif

        delete _prev_frame;

        /* Clean up framebuffer stuff */
        _buffer_locker->Lock();
#ifdef DRAWTHREAD
        wait_for_thread(_draw_thread_id, &result);
#endif
        free(_clips);
        delete _buffer_locker;
    }


    /* * * * * OpenGL functionality * * * * */
#if SDL_VIDEO_OPENGL
    virtual BGLView *CreateGLView(Uint32 gl_flags) {
        Lock();
        if (_SDL_GLView == NULL) {
            _SDL_GLView = new BGLView(Bounds(), "SDL GLView",
                                     B_FOLLOW_ALL_SIDES,
                                     (B_WILL_DRAW | B_FRAME_EVENTS),
                                     gl_flags);
            _gl_type = gl_flags;
        }
        AddChild(_SDL_GLView);
        _SDL_GLView->EnableDirectMode(true);
        _SDL_GLView->LockGL();  /* "New" GLViews are created */
        Unlock();
        return (_SDL_GLView);
    }

    virtual void RemoveGLView() {
        Lock();
        if(_SDL_GLView) {
            _SDL_GLView->UnlockGL();
            RemoveChild(_SDL_GLView);
        }
        Unlock();
    }

    virtual void SwapBuffers(void) {
        _SDL_GLView->UnlockGL();
        _SDL_GLView->LockGL();
        _SDL_GLView->SwapBuffers();
    }
#endif

    /* * * * * Framebuffering* * * * */
    virtual void DirectConnected(direct_buffer_info *info) {
        if(!_connected && _connection_disabled) {
            return;
        }

        /* Determine if the pixel buffer is usable after this update */
        _trash_window_buffer =      _trash_window_buffer
                                || ((info->buffer_state & B_BUFFER_RESIZED)
                                || (info->buffer_state & B_BUFFER_RESET)
                                || (info->driver_state == B_MODE_CHANGED));
        LockBuffer();

        switch(info->buffer_state & B_DIRECT_MODE_MASK) {
        case B_DIRECT_START:
            _connected = true;

        case B_DIRECT_MODIFY:
            if(_clips) {
                free(_clips);
                _clips = NULL;
            }

            _num_clips = info->clip_list_count;
            _clips = (clipping_rect *)malloc(_num_clips*sizeof(clipping_rect));
            if(_clips) {
                memcpy(_clips, info->clip_list,
                    _num_clips*sizeof(clipping_rect));

                _bits = (uint8*) info->bits;
                _row_bytes = info->bytes_per_row;
                _bounds = info->window_bounds;
                _bytes_per_px = info->bits_per_pixel / 8;
                _buffer_dirty = true;
            }
            break;

        case B_DIRECT_STOP:
            _connected = false;
            break;
        }
#if SDL_VIDEO_OPENGL
        if(_SDL_GLView) {
            _SDL_GLView->DirectConnected(info);
        }
#endif


        /* Call the base object directconnected */
        BDirectWindow::DirectConnected(info);

        UnlockBuffer();

    }




    /* * * * * Event sending * * * * */
    /* Hook functions */
    virtual void FrameMoved(BPoint origin) {
        /* Post a message to the BApp so that it can handle the window event */
        BMessage msg(BAPP_WINDOW_MOVED);
        msg.AddInt32("window-x", (int)origin.x);
        msg.AddInt32("window-y", (int)origin.y);
        _PostWindowEvent(msg);

        /* Perform normal hook operations */
        BDirectWindow::FrameMoved(origin);
    }

    virtual void FrameResized(float width, float height) {
        /* Post a message to the BApp so that it can handle the window event */
        BMessage msg(BAPP_WINDOW_RESIZED);

        msg.AddInt32("window-w", (int)width + 1);
        msg.AddInt32("window-h", (int)height + 1);
        _PostWindowEvent(msg);

        /* Perform normal hook operations */
        BDirectWindow::FrameResized(width, height);
    }

    virtual bool QuitRequested() {
        BMessage msg(BAPP_WINDOW_CLOSE_REQUESTED);
        _PostWindowEvent(msg);

        /* We won't allow a quit unless asked by DestroyWindow() */
        return false;
    }

    virtual void WindowActivated(bool active) {
        BMessage msg(BAPP_KEYBOARD_FOCUS);  /* Mouse focus sold separately */
        msg.AddBool("focusGained", active);
        _PostWindowEvent(msg);
    }

    virtual void Zoom(BPoint origin,
                float width,
                float height) {
        BMessage msg(BAPP_MAXIMIZE);    /* Closest thing to maximization Haiku has */
        _PostWindowEvent(msg);

        /* Before the window zooms, record its size */
        if( !_prev_frame )
            _prev_frame = new BRect(Frame());

        /* Perform normal hook operations */
        BDirectWindow::Zoom(origin, width, height);
    }

    /* Member functions */
    virtual void Show() {
        while(IsHidden()) {
            BDirectWindow::Show();
        }
        _shown = true;

        BMessage msg(BAPP_SHOW);
        _PostWindowEvent(msg);
    }

    virtual void Hide() {
        BDirectWindow::Hide();
        _shown = false;

        BMessage msg(BAPP_HIDE);
        _PostWindowEvent(msg);
    }

    virtual void Minimize(bool minimize) {
        BDirectWindow::Minimize(minimize);
        int32 minState = (minimize ? BAPP_MINIMIZE : BAPP_RESTORE);

        BMessage msg(minState);
        _PostWindowEvent(msg);
    }


    /* BView message interruption */
    virtual void DispatchMessage(BMessage * msg, BHandler * target)
    {
        BPoint where;   /* Used by mouse moved */
        int32 buttons;  /* Used for mouse button events */
        int32 key;      /* Used for key events */

        switch (msg->what) {
        case B_MOUSE_MOVED:
            int32 transit;
            if (msg->FindPoint("where", &where) == B_OK
                && msg->FindInt32("be:transit", &transit) == B_OK) {
                _MouseMotionEvent(where, transit);
            }

            /* FIXME: Apparently a button press/release event might be dropped
               if made before before a different button is released.  Does
               B_MOUSE_MOVED have the data needed to check if a mouse button
               state has changed? */
            if (msg->FindInt32("buttons", &buttons) == B_OK) {
                _MouseButtonEvent(buttons);
            }
            break;

        case B_MOUSE_DOWN:
        case B_MOUSE_UP:
            /* _MouseButtonEvent() detects any and all buttons that may have
               changed state, as well as that button's new state */
            if (msg->FindInt32("buttons", &buttons) == B_OK) {
                _MouseButtonEvent(buttons);
            }
            break;

        case B_MOUSE_WHEEL_CHANGED:
            float x, y;
            if (msg->FindFloat("be:wheel_delta_x", &x) == B_OK
                && msg->FindFloat("be:wheel_delta_y", &y) == B_OK) {
                    _MouseWheelEvent((int)x, (int)y);
            }
            break;

        case B_KEY_DOWN:
            {
                int32 i = 0;
                int8 byte;
                int8 bytes[4] = { 0, 0, 0, 0 };
                while (i < 4 && msg->FindInt8("byte", i, &byte) == B_OK) {
                    bytes[i] = byte;
                    i++;
                }
                if (msg->FindInt32("key", &key) == B_OK) {
                    _KeyEvent((SDL_Scancode)key, &bytes[0], i, SDL_PRESSED);
                }
            }
            break;
            
        case B_UNMAPPED_KEY_DOWN:      /* modifier keys are unmapped */
            if (msg->FindInt32("key", &key) == B_OK) {
                _KeyEvent((SDL_Scancode)key, NULL, 0, SDL_PRESSED);
            }
            break;

        case B_KEY_UP:
        case B_UNMAPPED_KEY_UP:        /* modifier keys are unmapped */
            if (msg->FindInt32("key", &key) == B_OK) {
                _KeyEvent(key, NULL, 0, SDL_RELEASED);
            }
            break;

        default:
            /* move it after switch{} so it's always handled
               that way we keep Haiku features like:
               - CTRL+Q to close window (and other shortcuts)
               - PrintScreen to make screenshot into /boot/home
               - etc.. */
            /* BDirectWindow::DispatchMessage(msg, target); */
            break;
        }

        BDirectWindow::DispatchMessage(msg, target);
    }

    /* Handle command messages */
    virtual void MessageReceived(BMessage* message) {
        switch (message->what) {
            /* Handle commands from SDL */
            case BWIN_SET_TITLE:
                _SetTitle(message);
                break;
            case BWIN_MOVE_WINDOW:
                _MoveTo(message);
                break;
            case BWIN_RESIZE_WINDOW:
                _ResizeTo(message);
                break;
            case BWIN_SET_BORDERED:
                _SetBordered(message);
                break;
            case BWIN_SET_RESIZABLE:
                _SetResizable(message);
                break;
            case BWIN_SHOW_WINDOW:
                Show();
                break;
            case BWIN_HIDE_WINDOW:
                Hide();
                break;
            case BWIN_MAXIMIZE_WINDOW:
                BWindow::Zoom();
                break;
            case BWIN_MINIMIZE_WINDOW:
                Minimize(true);
                break;
            case BWIN_RESTORE_WINDOW:
                _Restore();
                break;
            case BWIN_FULLSCREEN:
                _SetFullScreen(message);
                break;
            default:
                /* Perform normal message handling */
                BDirectWindow::MessageReceived(message);
                break;
        }

    }



    /* Accessor methods */
    bool IsShown() { return _shown; }
    int32 GetID() { return _id; }
    uint32 GetRowBytes() { return _row_bytes; }
    int32 GetFbX() { return _bounds.left; }
    int32 GetFbY() { return _bounds.top; }
    bool ConnectionEnabled() { return !_connection_disabled; }
    bool Connected() { return _connected; }
    clipping_rect *GetClips() { return _clips; }
    int32 GetNumClips() { return _num_clips; }
    uint8* GetBufferPx() { return _bits; }
    int32 GetBytesPerPx() { return _bytes_per_px; }
    bool CanTrashWindowBuffer() { return _trash_window_buffer; }
    bool BufferExists() { return _buffer_created; }
    bool BufferIsDirty() { return _buffer_dirty; }
    BBitmap *GetBitmap() { return _bitmap; }
#if SDL_VIDEO_OPENGL
    BGLView *GetGLView() { return _SDL_GLView; }
    Uint32 GetGLType() { return _gl_type; }
#endif

    /* Setter methods */
    void SetID(int32 id) { _id = id; }
    void SetBufferExists(bool bufferExists) { _buffer_created = bufferExists; }
    void LockBuffer() { _buffer_locker->Lock(); }
    void UnlockBuffer() { _buffer_locker->Unlock(); }
    void SetBufferDirty(bool bufferDirty) { _buffer_dirty = bufferDirty; }
    void SetTrashBuffer(bool trash) { _trash_window_buffer = trash;     }
    void SetBitmap(BBitmap *bitmap) { _bitmap = bitmap; }


private:
    /* Event redirection */
    void _MouseMotionEvent(BPoint &where, int32 transit) {
        if(transit == B_EXITED_VIEW) {
            /* Change mouse focus */
            if(_mouse_focused) {
                _MouseFocusEvent(false);
            }
        } else {
            /* Change mouse focus */
            if (!_mouse_focused) {
                _MouseFocusEvent(true);
            }
            BMessage msg(BAPP_MOUSE_MOVED);
            msg.AddInt32("x", (int)where.x);
            msg.AddInt32("y", (int)where.y);

            _PostWindowEvent(msg);
        }
    }

    void _MouseFocusEvent(bool focusGained) {
        _mouse_focused = focusGained;
        BMessage msg(BAPP_MOUSE_FOCUS);
        msg.AddBool("focusGained", focusGained);
        _PostWindowEvent(msg);

/* FIXME: Why were these here?
 if false: be_app->SetCursor(B_HAND_CURSOR);
 if true:  SDL_SetCursor(NULL); */
    }

    void _MouseButtonEvent(int32 buttons) {
        int32 buttonStateChange = buttons ^ _last_buttons;

        /* Make sure at least one button has changed state */
        if( !(buttonStateChange) ) {
            return;
        }

        /* Add any mouse button events */
        if(buttonStateChange & B_PRIMARY_MOUSE_BUTTON) {
            _SendMouseButton(SDL_BUTTON_LEFT, buttons &
                B_PRIMARY_MOUSE_BUTTON);
        }
        if(buttonStateChange & B_SECONDARY_MOUSE_BUTTON) {
            _SendMouseButton(SDL_BUTTON_RIGHT, buttons &
                B_PRIMARY_MOUSE_BUTTON);
        }
        if(buttonStateChange & B_TERTIARY_MOUSE_BUTTON) {
            _SendMouseButton(SDL_BUTTON_MIDDLE, buttons &
                B_PRIMARY_MOUSE_BUTTON);
        }

        _last_buttons = buttons;
    }

    void _SendMouseButton(int32 button, int32 state) {
        BMessage msg(BAPP_MOUSE_BUTTON);
        msg.AddInt32("button-id", button);
        msg.AddInt32("button-state", state);
        _PostWindowEvent(msg);
    }

    void _MouseWheelEvent(int32 x, int32 y) {
        /* Create a message to pass along to the BeApp thread */
        BMessage msg(BAPP_MOUSE_WHEEL);
        msg.AddInt32("xticks", x);
        msg.AddInt32("yticks", y);
        _PostWindowEvent(msg);
    }

    void _KeyEvent(int32 keyCode, const int8 *keyUtf8, const ssize_t & len, int32 keyState) {
        /* Create a message to pass along to the BeApp thread */
        BMessage msg(BAPP_KEY);
        msg.AddInt32("key-state", keyState);
        msg.AddInt32("key-scancode", keyCode);
        if (keyUtf8 != NULL) {
            msg.AddData("key-utf8", B_INT8_TYPE, (const void*)keyUtf8, len);
        }
        be_app->PostMessage(&msg);
    }

    void _RepaintEvent() {
        /* Force a repaint: Call the SDL exposed event */
        BMessage msg(BAPP_REPAINT);
        _PostWindowEvent(msg);
    }
    void _PostWindowEvent(BMessage &msg) {
        msg.AddInt32("window-id", _id);
        be_app->PostMessage(&msg);
    }

    /* Command methods (functions called upon by SDL) */
    void _SetTitle(BMessage *msg) {
        const char *title;
        if(
            msg->FindString("window-title", &title) != B_OK
        ) {
            return;
        }
        SetTitle(title);
    }

    void _MoveTo(BMessage *msg) {
        int32 x, y;
        if(
            msg->FindInt32("window-x", &x) != B_OK ||
            msg->FindInt32("window-y", &y) != B_OK
        ) {
            return;
        }
        MoveTo(x, y);
    }

    void _ResizeTo(BMessage *msg) {
        int32 w, h;
        if(
            msg->FindInt32("window-w", &w) != B_OK ||
            msg->FindInt32("window-h", &h) != B_OK
        ) {
            return;
        }
        ResizeTo(w, h);
    }

    void _SetBordered(BMessage *msg) {
        bool bEnabled;
        if(msg->FindBool("window-border", &bEnabled) != B_OK) {
            return;
        }
        SetLook(bEnabled ? B_TITLED_WINDOW_LOOK : B_NO_BORDER_WINDOW_LOOK);
    }

    void _SetResizable(BMessage *msg) {
        bool bEnabled;
        if(msg->FindBool("window-resizable", &bEnabled) != B_OK) {
            return;
        }
        if (bEnabled) {
            SetFlags(Flags() & ~(B_NOT_RESIZABLE | B_NOT_ZOOMABLE));
        } else {
            SetFlags(Flags() | (B_NOT_RESIZABLE | B_NOT_ZOOMABLE));
        }
    }

    void _Restore() {
        if(IsMinimized()) {
            Minimize(false);
        } else if(IsHidden()) {
            Show();
        } else if(_prev_frame != NULL) {    /* Zoomed */
            MoveTo(_prev_frame->left, _prev_frame->top);
            ResizeTo(_prev_frame->Width(), _prev_frame->Height());
        }
    }

    void _SetFullScreen(BMessage *msg) {
        bool fullscreen;
        if(
            msg->FindBool("fullscreen", &fullscreen) != B_OK
        ) {
            return;
        }
        SetFullScreen(fullscreen);
    }

    /* Members */
#if SDL_VIDEO_OPENGL
    BGLView * _SDL_GLView;
    Uint32 _gl_type;
#endif

    int32 _last_buttons;
    int32 _id;  /* Window id used by SDL_BApp */
    bool  _mouse_focused;       /* Does this window have mouse focus? */
    bool  _shown;
    bool  _inhibit_resize;

    BRect *_prev_frame; /* Previous position and size of the window */

    /* Framebuffer members */
    bool            _connected,
                    _connection_disabled,
                    _buffer_created,
                    _buffer_dirty,
                    _trash_window_buffer;
    uint8          *_bits;
    uint32          _row_bytes;
    clipping_rect   _bounds;
    BLocker        *_buffer_locker;
    clipping_rect  *_clips;
    int32           _num_clips;
    int32           _bytes_per_px;
    thread_id       _draw_thread_id;

    BBitmap        *_bitmap;
};


/* FIXME:
 * An explanation of framebuffer flags.
 *
 * _connected -           Original variable used to let the drawing thread know
 *                         when changes are being made to the other framebuffer
 *                         members.
 * _connection_disabled - Used to signal to the drawing thread that the window
 *                         is closing, and the thread should exit.
 * _buffer_created -      True if the current buffer is valid
 * _buffer_dirty -        True if the window should be redrawn.
 * _trash_window_buffer - True if the window buffer needs to be trashed partway
 *                         through a draw cycle.  Occurs when the previous
 *                         buffer provided by DirectConnected() is invalidated.
 */
#endif /* SDL_BWin_h_ */

/* vi: set ts=4 sw=4 expandtab: */
