/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2017 - ROLI Ltd.

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 5 End-User License
   Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
   27th April 2017).

   End User License Agreement: www.juce.com/juce-5-licence
   Privacy Policy: www.juce.com/juce-5-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

typedef void (*WindowMessageReceiveCallback) (XEvent&);
WindowMessageReceiveCallback dispatchWindowMessage = nullptr;

typedef void (*SelectionRequestCallback) (XSelectionRequestEvent&);
SelectionRequestCallback handleSelectionRequest = nullptr;

::Window juce_messageWindowHandle;
XContext windowHandleXContext;

//==============================================================================
namespace X11ErrorHandling
{
    static XErrorHandler   oldErrorHandler = {};
    static XIOErrorHandler oldIOErrorHandler = {};

    //==============================================================================
    // Usually happens when client-server connection is broken
    int ioErrorHandler (::Display*)
    {
        DBG ("ERROR: connection to X server broken.. terminating.");

        if (JUCEApplicationBase::isStandaloneApp())
            MessageManager::getInstance()->stopDispatchLoop();

        return 0;
    }


    int errorHandler (::Display* display, XErrorEvent* event)
    {
        ignoreUnused (display, event);

       #if JUCE_DEBUG_XERRORS
        char errorStr[64] = { 0 };
        char requestStr[64] = { 0 };

        XGetErrorText (display, event->error_code, errorStr, 64);
        XGetErrorDatabaseText (display, "XRequest", String (event->request_code).toUTF8(), "Unknown", requestStr, 64);
        DBG ("ERROR: X returned " << errorStr << " for operation " << requestStr);
       #endif

        return 0;
    }

    void installXErrorHandlers()
    {
        oldIOErrorHandler = XSetIOErrorHandler (ioErrorHandler);
        oldErrorHandler = XSetErrorHandler (errorHandler);
    }

    void removeXErrorHandlers()
    {
        XSetIOErrorHandler (oldIOErrorHandler);
        oldIOErrorHandler = {};

        XSetErrorHandler (oldErrorHandler);
        oldErrorHandler = {};
    }
}

//==============================================================================
XWindowSystem::XWindowSystem() noexcept
{
    if (JUCEApplicationBase::isStandaloneApp())
    {
        // Initialise xlib for multiple thread support
        static bool initThreadCalled = false;

        if (! initThreadCalled)
        {
            if (! XInitThreads())
            {
                // This is fatal!  Print error and closedown
                Logger::outputDebugString ("Failed to initialise xlib thread support.");
                Process::terminate();
                return;
            }

            initThreadCalled = true;
        }

        X11ErrorHandling::installXErrorHandlers();
    }
}

XWindowSystem::~XWindowSystem() noexcept
{
    if (JUCEApplicationBase::isStandaloneApp())
        X11ErrorHandling::removeXErrorHandlers();

    clearSingletonInstance();
}

::Display* XWindowSystem::displayRef() noexcept
{
    if (++displayCount == 1)
    {
        jassert (display == nullptr);

        String displayName (getenv ("DISPLAY"));

        if (displayName.isEmpty())
            displayName = ":0.0";

        // it seems that on some systems XOpenDisplay will occasionally
        // fail the first time, but succeed on a second attempt..
        for (int retries = 2; --retries >= 0;)
        {
            display = XOpenDisplay (displayName.toUTF8());

            if (display != nullptr)
                break;
        }

        initialiseXDisplay();
    }

    return display;
}

::Display* XWindowSystem::displayUnref() noexcept
{
    jassert (display != nullptr);
    jassert (displayCount.get() > 0);

    if (--displayCount == 0)
    {
        destroyXDisplay();
        XCloseDisplay (display);
        display = nullptr;
    }

    return display;
}

void XWindowSystem::initialiseXDisplay() noexcept
{
    // This is fatal!  Print error and closedown
    if (display == nullptr)
    {
        Logger::outputDebugString ("Failed to connect to the X Server.");
        Process::terminate();
    }

    // Create a context to store user data associated with Windows we create
    windowHandleXContext = XUniqueContext();

    // We're only interested in client messages for this window, which are always sent
    XSetWindowAttributes swa;
    swa.event_mask = NoEventMask;

    // Create our message window (this will never be mapped)
    const int screen = DefaultScreen (display);
    juce_messageWindowHandle = XCreateWindow (display, RootWindow (display, screen),
                                              0, 0, 1, 1, 0, 0, InputOnly,
                                              DefaultVisual (display, screen),
                                              CWEventMask, &swa);

    XSync (display, False);

    // Setup input event handler
    int fd = XConnectionNumber (display);

    LinuxEventLoop::registerFdCallback (fd,
         [this](int)
         {
            do
            {
                XEvent evt;

                {
                    ScopedXLock xlock (display);

                    if (! XPending (display))
                        return;

                    XNextEvent (display, &evt);
                }

                if (evt.type == SelectionRequest && evt.xany.window == juce_messageWindowHandle
                     && handleSelectionRequest != nullptr)
                {
                    handleSelectionRequest (evt.xselectionrequest);
                }
                else if (evt.xany.window != juce_messageWindowHandle
                          && dispatchWindowMessage != nullptr)
                {
                    dispatchWindowMessage (evt);
                }

            } while (display != nullptr);
        });
}

void XWindowSystem::destroyXDisplay() noexcept
{
    ScopedXLock xlock (display);
    XDestroyWindow (display, juce_messageWindowHandle);
    juce_messageWindowHandle = 0;
    XSync (display, True);
    LinuxEventLoop::unregisterFdCallback (XConnectionNumber (display));
}

JUCE_IMPLEMENT_SINGLETON (XWindowSystem)

//==============================================================================
ScopedXDisplay::ScopedXDisplay() : display (XWindowSystem::getInstance()->displayRef())
{
}

ScopedXDisplay::~ScopedXDisplay()
{
    XWindowSystem::getInstance()->displayUnref();
}

//==============================================================================
ScopedXLock::ScopedXLock (::Display* d) : display (d)
{
    if (display != nullptr)
        XLockDisplay (display);
}

ScopedXLock::~ScopedXLock()
{
    if (display != nullptr)
        XUnlockDisplay (display);
}

//==============================================================================
Atoms::Atoms (::Display* display)
{
    protocols                    = getIfExists (display, "WM_PROTOCOLS");
    protocolList [TAKE_FOCUS]    = getIfExists (display, "WM_TAKE_FOCUS");
    protocolList [DELETE_WINDOW] = getIfExists (display, "WM_DELETE_WINDOW");
    protocolList [PING]          = getIfExists (display, "_NET_WM_PING");
    changeState                  = getIfExists (display, "WM_CHANGE_STATE");
    state                        = getIfExists (display, "WM_STATE");
    userTime                     = getCreating (display, "_NET_WM_USER_TIME");
    activeWin                    = getCreating (display, "_NET_ACTIVE_WINDOW");
    pid                          = getCreating (display, "_NET_WM_PID");
    windowType                   = getIfExists (display, "_NET_WM_WINDOW_TYPE");
    windowState                  = getIfExists (display, "_NET_WM_STATE");

    XdndAware                    = getCreating (display, "XdndAware");
    XdndEnter                    = getCreating (display, "XdndEnter");
    XdndLeave                    = getCreating (display, "XdndLeave");
    XdndPosition                 = getCreating (display, "XdndPosition");
    XdndStatus                   = getCreating (display, "XdndStatus");
    XdndDrop                     = getCreating (display, "XdndDrop");
    XdndFinished                 = getCreating (display, "XdndFinished");
    XdndSelection                = getCreating (display, "XdndSelection");

    XdndTypeList                 = getCreating (display, "XdndTypeList");
    XdndActionList               = getCreating (display, "XdndActionList");
    XdndActionCopy               = getCreating (display, "XdndActionCopy");
    XdndActionPrivate            = getCreating (display, "XdndActionPrivate");
    XdndActionDescription        = getCreating (display, "XdndActionDescription");

    XembedMsgType                = getCreating (display, "_XEMBED");
    XembedInfo                   = getCreating (display, "_XEMBED_INFO");

    allowedMimeTypes[0]          = getCreating (display, "UTF8_STRING");
    allowedMimeTypes[1]          = getCreating (display, "text/plain;charset=utf-8");
    allowedMimeTypes[2]          = getCreating (display, "text/plain");
    allowedMimeTypes[3]          = getCreating (display, "text/uri-list");

    allowedActions[0]            = getCreating (display, "XdndActionMove");
    allowedActions[1]            = XdndActionCopy;
    allowedActions[2]            = getCreating (display, "XdndActionLink");
    allowedActions[3]            = getCreating (display, "XdndActionAsk");
    allowedActions[4]            = XdndActionPrivate;
}

Atom Atoms::getIfExists (::Display* display, const char* name)  { return XInternAtom (display, name, True); }
Atom Atoms::getCreating (::Display* display, const char* name)  { return XInternAtom (display, name, False); }

String Atoms::getName (::Display* display, const Atom atom)
{
    if (atom == None)
        return "None";

    return String (XGetAtomName (display, atom));
}

bool Atoms::isMimeTypeFile (::Display* display, const Atom atom)
{
    return getName (display, atom).equalsIgnoreCase ("text/uri-list");
}


const unsigned long Atoms::DndVersion = 3;

//==============================================================================
GetXProperty::GetXProperty (::Display* display, Window window, Atom atom,
              long offset, long length, bool shouldDelete,
              Atom requestedType)
{
    success = (XGetWindowProperty (display, window, atom, offset, length,
                                   (Bool) shouldDelete, requestedType, &actualType,
                                   &actualFormat, &numItems, &bytesLeft, &data) == Success)
                && data != nullptr;
}

GetXProperty::~GetXProperty()
{
    if (data != nullptr)
        XFree (data);
}

} // namespace juce
