/*
 * DISTRHO Plugin Framework (DPF)
 * Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any purpose with
 * or without fee is hereby granted, provided that the above copyright notice and this
 * permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
 * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
 * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

// needed for IDE
#include "DistrhoPluginInfo.h"

#include "DistrhoUI.hpp"

#if defined(DISTRHO_OS_MAC)
# import <Cocoa/Cocoa.h>
#elif defined(DISTRHO_OS_WINDOWS)
# define WIN32_CLASS_NAME "DPF-EmbedExternalExampleUI"
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <sys/types.h>
# include <X11/Xatom.h>
# include <X11/Xlib.h>
# include <X11/Xutil.h>
# define X11Key_Escape 9
#endif

#if defined(DISTRHO_OS_MAC)
# ifndef __MAC_10_12
#  define NSEventMaskAny NSAnyEventMask
#  define NSWindowStyleMaskClosable NSClosableWindowMask
#  define NSWindowStyleMaskMiniaturizable NSMiniaturizableWindowMask
#  define NSWindowStyleMaskResizable NSResizableWindowMask
#  define NSWindowStyleMaskTitled NSTitledWindowMask
# endif
@interface NSExternalWindow : NSWindow
@end
@implementation NSExternalWindow {
@public
    bool closed;
    bool standalone;
}
- (BOOL)canBecomeKeyWindow { return YES; }
- (BOOL)canBecomeMainWindow { return standalone ? YES : NO; }
- (BOOL)windowShouldClose:(id)_ { closed = true; return YES; }
@end
#endif

START_NAMESPACE_DISTRHO

// -----------------------------------------------------------------------------------------------------------

class EmbedExternalExampleUI : public UI
{
#if defined(DISTRHO_OS_MAC)
    NSView* fView;
    NSExternalWindow* fWindow;
#elif defined(DISTRHO_OS_WINDOWS)
    ::HWND fWindow;
#else
    ::Display* fDisplay;
    ::Window fWindow;
#endif

public:
    EmbedExternalExampleUI()
        : UI(512, 256),
#if defined(DISTRHO_OS_MAC)
          fView(nullptr),
          fWindow(nullptr)
#elif defined(DISTRHO_OS_WINDOWS)
          fWindow(nullptr)
#else
          fDisplay(nullptr),
          fWindow(0)
#endif
    {
        const bool standalone = isStandalone();
        d_stdout("isStandalone %d", (int)standalone);

#if defined(DISTRHO_OS_MAC)
        NSAutoreleasePool* const pool = [[NSAutoreleasePool alloc]init];
        [NSApplication sharedApplication];

        if (standalone)
        {
            [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
            [NSApp activateIgnoringOtherApps:YES];
        }

        fView = [NSView new];
        DISTRHO_SAFE_ASSERT_RETURN(fView != nullptr,);

        [fView setFrame:NSMakeRect(0, 0, getWidth(), getHeight())];
        [fView setAutoresizesSubviews:YES];
        [fView setWantsLayer:YES];
        [[fView layer] setBackgroundColor:[[NSColor blueColor] CGColor]];

        if (isEmbed())
        {
            [fView retain];
            [(NSView*)getParentWindowHandle() addSubview:fView];
        }
        else
        {
            const ulong styleMask = NSWindowStyleMaskClosable
                                  | NSWindowStyleMaskMiniaturizable
                                  | NSWindowStyleMaskResizable
                                  | NSWindowStyleMaskTitled;

            fWindow = [[[NSExternalWindow alloc]
                         initWithContentRect:[fView frame]
                                   styleMask:styleMask
                                     backing:NSBackingStoreBuffered
                                       defer:NO]retain];
            DISTRHO_SAFE_ASSERT_RETURN(fWindow != nullptr,);

            fWindow->closed = false; // is this needed?
            fWindow->standalone = standalone;
            [fWindow setIsVisible:NO];

            if (NSString* const nsTitle = [[NSString alloc]
                                            initWithBytes:getTitle()
                                                   length:strlen(getTitle())
                                                 encoding:NSUTF8StringEncoding])
                [fWindow setTitle:nsTitle];

            [fWindow setContentView:fView];
            [fWindow setContentSize:NSMakeSize(getWidth(), getHeight())];
            [fWindow makeFirstResponder:fView];
        }

        [pool release];
#elif defined(DISTRHO_OS_WINDOWS)
        WNDCLASS windowClass = {};
        windowClass.style         = CS_OWNDC;
        windowClass.lpfnWndProc   = DefWindowProc;
        windowClass.hInstance     = nullptr;
        windowClass.hIcon         = LoadIcon(nullptr, IDI_APPLICATION);
        windowClass.hCursor       = LoadCursor(nullptr, IDC_ARROW);
        windowClass.lpszClassName = WIN32_CLASS_NAME;
        DISTRHO_SAFE_ASSERT_RETURN(RegisterClass(&windowClass),);

        const int winFlags = isEmbed() ? (WS_CHILD | WS_VISIBLE) : (WS_POPUPWINDOW | WS_CAPTION | WS_SIZEBOX);

        RECT rect = { 0, 0, static_cast<LONG>(getWidth()), static_cast<LONG>(getHeight()) };
        AdjustWindowRectEx(&rect, winFlags, FALSE, WS_EX_TOPMOST);

        fWindow = CreateWindowEx(WS_EX_TOPMOST,
                                 WIN32_CLASS_NAME,
                                 getTitle(),
                                 winFlags,
                                 CW_USEDEFAULT, CW_USEDEFAULT,
                                 rect.right - rect.left,
                                 rect.bottom - rect.top,
                                 (HWND)getParentWindowHandle(),
                                 nullptr, nullptr, nullptr);
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != nullptr,);

        SetWindowLongPtr(fWindow, GWLP_USERDATA, (LONG_PTR)this);
#else
        fDisplay = XOpenDisplay(nullptr);
        DISTRHO_SAFE_ASSERT_RETURN(fDisplay != nullptr,);

        const ::Window parent = isEmbed()
                              ? (::Window)getParentWindowHandle()
                              : RootWindow(fDisplay, DefaultScreen(fDisplay));

        fWindow = XCreateSimpleWindow(fDisplay, parent, 0, 0, getWidth(), getHeight(), 0, 0, 0);
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != 0,);

        XSizeHints sizeHints = {};
        sizeHints.flags      = PMinSize;
        sizeHints.min_width  = getWidth();
        sizeHints.min_height = getHeight();
        XSetNormalHints(fDisplay, fWindow, &sizeHints);
        XStoreName(fDisplay, fWindow, getTitle());

        if (isEmbed())
        {
            // start with window mapped, so host can access it
            XMapWindow(fDisplay, fWindow);
        }
        else
        {
            // grab Esc key for auto-close
            XGrabKey(fDisplay, X11Key_Escape, AnyModifier, fWindow, 1, GrabModeAsync, GrabModeAsync);

            // destroy window on close
            Atom wmDelete = XInternAtom(fDisplay, "WM_DELETE_WINDOW", True);
            XSetWMProtocols(fDisplay, fWindow, &wmDelete, 1);

            // set pid WM hint
            const pid_t pid = getpid();
            const Atom _nwp = XInternAtom(fDisplay, "_NET_WM_PID", False);
            XChangeProperty(fDisplay, fWindow, _nwp, XA_CARDINAL, 32, PropModeReplace, (const uchar*)&pid, 1);

            // set the window to both dialog and normal to produce a decorated floating dialog
            // order is important: DIALOG needs to come before NORMAL
            const Atom _wt = XInternAtom(fDisplay, "_NET_WM_WINDOW_TYPE", False);
            const Atom _wts[2] = {
                XInternAtom(fDisplay, "_NET_WM_WINDOW_TYPE_DIALOG", False),
                XInternAtom(fDisplay, "_NET_WM_WINDOW_TYPE_NORMAL", False)
            };
            XChangeProperty(fDisplay, fWindow, _wt, XA_ATOM, 32, PropModeReplace, (const uchar*)&_wts, 2);
        }
#endif
        d_stdout("created external window with size %u %u", getWidth(), getHeight());
    }

    ~EmbedExternalExampleUI()
    {
#if defined(DISTRHO_OS_MAC)
        if (fView == nullptr)
            return;

        if (fWindow != nil)
            [fWindow close];

        [fView release];

        if (fWindow != nil)
            [fWindow release];
#elif defined(DISTRHO_OS_WINDOWS)
        if (fWindow != nullptr)
            DestroyWindow(fWindow);

        UnregisterClass(WIN32_CLASS_NAME, nullptr);
#else
        if (fDisplay == nullptr)
            return;

        if (fWindow != 0)
            XDestroyWindow(fDisplay, fWindow);

        XCloseDisplay(fDisplay);
#endif
    }

protected:
   /* --------------------------------------------------------------------------------------------------------
    * DSP/Plugin Callbacks */

   /**
      A parameter has changed on the plugin side.
      This is called by the host to inform the UI about parameter changes.
    */
    void parameterChanged(uint32_t index, float value) override
    {
        d_stdout("parameterChanged %u %f", index, value);

        switch (index)
        {
        case kParameterWidth:
            setWidth(static_cast<int>(value + 0.5f));
            break;
        case kParameterHeight:
            setHeight(static_cast<int>(value + 0.5f));
            break;
        }
    }

   /* --------------------------------------------------------------------------------------------------------
    * External Window overrides */

    void focus() override
    {
        d_stdout("focus");
#if defined(DISTRHO_OS_MAC)
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != nullptr,);

        [fWindow orderFrontRegardless];
        [fWindow makeKeyWindow];
        [fWindow makeFirstResponder:fView];
#elif defined(DISTRHO_OS_WINDOWS)
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != nullptr,);

        SetForegroundWindow(fWindow);
        SetActiveWindow(fWindow);
        SetFocus(fWindow);
#else
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != 0,);

        XRaiseWindow(fDisplay, fWindow);
#endif
    }

    uintptr_t getNativeWindowHandle() const noexcept override
    {
#if defined(DISTRHO_OS_MAC)
        return (uintptr_t)fView;
#elif defined(DISTRHO_OS_WINDOWS)
#else
        return (uintptr_t)fWindow;
#endif
        return 0;
    }

    void sizeChanged(uint width, uint height) override
    {
        d_stdout("sizeChanged %u %u", width, height);
        UI::sizeChanged(width, height);

#if defined(DISTRHO_OS_MAC)
        NSRect rect = [fView frame];
        rect.size = CGSizeMake((CGFloat)width, (CGFloat)height);
        [fView setFrame:rect];
#elif defined(DISTRHO_OS_WINDOWS)
        if (fWindow != nullptr)
            SetWindowPos(fWindow,
                         HWND_TOP,
                         0, 0,
                         width, height,
                         SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER);
#else
        if (fWindow != 0)
            XResizeWindow(fDisplay, fWindow, width, height);
#endif
    }

    void titleChanged(const char* const title) override
    {
        d_stdout("titleChanged %s", title);
#if defined(DISTRHO_OS_MAC)
        if (fWindow != nil)
        {
            if (NSString* const nsTitle = [[NSString alloc]
                                           initWithBytes:title
                                                  length:strlen(title)
                                                encoding:NSUTF8StringEncoding])
            {
                [fWindow setTitle:nsTitle];
                [nsTitle release];
            }
        }
#elif defined(DISTRHO_OS_WINDOWS)
#else
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != 0,);
        XStoreName(fDisplay, fWindow, title);
#endif
    }

    void transientParentWindowChanged(const uintptr_t winId) override
    {
        d_stdout("transientParentWindowChanged %lu", winId);
#if defined(DISTRHO_OS_MAC)
#elif defined(DISTRHO_OS_WINDOWS)
#else
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != 0,);
        XSetTransientForHint(fDisplay, fWindow, (::Window)winId);
#endif
    }

    void visibilityChanged(const bool visible) override
    {
        d_stdout("visibilityChanged %d", visible);
#if defined(DISTRHO_OS_MAC)
        DISTRHO_SAFE_ASSERT_RETURN(fView != nullptr,);
        if (fWindow != nil)
        {
            [fWindow setIsVisible:(visible ? YES : NO)];

            if (isStandalone())
                [fWindow makeMainWindow];

            [fWindow makeKeyAndOrderFront:fWindow];
        }
        else
        {
            [fView setHidden:(visible ? NO : YES)];
        }
#elif defined(DISTRHO_OS_WINDOWS)
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != nullptr,);

        ShowWindow(fWindow, visible ? SW_SHOWNORMAL : SW_HIDE);
#else
        DISTRHO_SAFE_ASSERT_RETURN(fWindow != 0,);

        if (visible)
            XMapRaised(fDisplay, fWindow);
        else
            XUnmapWindow(fDisplay, fWindow);
#endif
    }

    void uiIdle() override
    {
        // d_stdout("uiIdle");
#if defined(DISTRHO_OS_MAC)
        if (isEmbed()) {
            return;
        }

        NSAutoreleasePool* const pool = [[NSAutoreleasePool alloc] init];
        NSDate* const date = [NSDate distantPast];

        for (NSEvent* event;;)
        {
            event = [NSApp
                     nextEventMatchingMask:NSEventMaskAny
                                 untilDate:date
                                    inMode:NSDefaultRunLoopMode
                                   dequeue:YES];

            if (event == nil)
                break;

            [NSApp sendEvent:event];
        }

        if (fWindow->closed)
        {
            fWindow->closed = false;
            close();
        }

        [pool release];
#elif defined(DISTRHO_OS_WINDOWS)
        if (fWindow == nullptr)
            return;

        /*
        MSG msg;
        if (! ::PeekMessage(&msg, fWindow, 0, 0, PM_NOREMOVE))
            return true;

        if (::GetMessage(&msg, nullptr, 0, 0) >= 0)
        {
            if (msg.message == WM_QUIT)
                return false;

            //TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        */
#else
        if (fDisplay == nullptr)
            return;

        for (XEvent event; XPending(fDisplay) > 0;)
        {
            XNextEvent(fDisplay, &event);

            if (! isVisible())
                continue;

            switch (event.type)
            {
            case ClientMessage:
                if (char* const type = XGetAtomName(fDisplay, event.xclient.message_type))
                {
                    if (std::strcmp(type, "WM_PROTOCOLS") == 0)
                        hide();
                }
                break;

            case KeyRelease:
                if (event.xkey.keycode == X11Key_Escape)
                    hide();
                break;
            }
        }
#endif
    }

    // -------------------------------------------------------------------------------------------------------

private:
    // Current value, cached for when UI becomes visible
    float fValue;

   /**
      Set our UI class as non-copyable and add a leak detector just in case.
    */
    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EmbedExternalExampleUI)
};

/* ------------------------------------------------------------------------------------------------------------
 * UI entry point, called by DPF to create a new UI instance. */

UI* createUI()
{
    return new EmbedExternalExampleUI();
}

// -----------------------------------------------------------------------------------------------------------

END_NAMESPACE_DISTRHO
