/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */


#include <osl/thread.hxx>
#include <osl/conditn.hxx>
#include <osl/mutex.hxx>
#include <osl/diagnose.h>
#include <sal/log.hxx>

#include <cppu/Enterable.hxx>
#include <cppu/helper/purpenv/Environment.hxx>
#include <cppu/helper/purpenv/Mapping.hxx>
#include <memory>

namespace {

class InnerThread;
class OuterThread;

class AffineBridge : public cppu::Enterable
{
public:
    enum Msg
    {
        CB_DONE,
        CB_FPOINTER
    };

    Msg                   m_message;
    uno_EnvCallee       * m_pCallee;
    va_list             * m_pParam;

    osl::Mutex            m_innerMutex;
    oslThreadIdentifier   m_innerThreadId;
    std::unique_ptr<InnerThread> m_pInnerThread;
    osl::Condition        m_innerCondition;
    sal_Int32             m_enterCount;

    osl::Mutex            m_outerMutex;
    oslThreadIdentifier   m_outerThreadId;
    osl::Condition        m_outerCondition;
    std::unique_ptr<OuterThread> m_pOuterThread;

    explicit  AffineBridge();
    virtual  ~AffineBridge() override;

    virtual void  v_callInto_v(uno_EnvCallee * pCallee, va_list * pParam) override;
    virtual void  v_callOut_v (uno_EnvCallee * pCallee, va_list * pParam) override;

    virtual void  v_enter() override;
    virtual void  v_leave() override;

    virtual bool v_isValid(OUString * pReason) override;

    void innerDispatch();
    void outerDispatch(bool loop);
};

class InnerThread : public osl::Thread
{
    virtual void SAL_CALL run() override;

    AffineBridge * m_pAffineBridge;

public:
    explicit InnerThread(AffineBridge * threadEnvironment)
        : m_pAffineBridge(threadEnvironment)
        {
            create();
        }
};

}

void InnerThread::run()
{
    osl_setThreadName("UNO AffineBridge InnerThread");

    m_pAffineBridge->enter();
    m_pAffineBridge->innerDispatch();
    m_pAffineBridge->leave();
}

namespace {

class OuterThread : public osl::Thread
{
    virtual void SAL_CALL run() override;

    AffineBridge * m_pAffineBridge;

public:
    explicit OuterThread(AffineBridge * threadEnvironment);
};

}

OuterThread::OuterThread(AffineBridge * threadEnvironment)
    : m_pAffineBridge(threadEnvironment)
{
    create();
}

void OuterThread::run()
{
    osl_setThreadName("UNO AffineBridge OuterThread");

    osl::MutexGuard guard(m_pAffineBridge->m_outerMutex);

    m_pAffineBridge->m_outerThreadId = getIdentifier();
    m_pAffineBridge->outerDispatch(false);
    m_pAffineBridge->m_outerThreadId = 0;

    m_pAffineBridge->m_pOuterThread = nullptr;
    m_pAffineBridge = nullptr;
}


AffineBridge::AffineBridge()
    : m_message      (CB_DONE),
      m_pCallee      (nullptr),
      m_pParam       (nullptr),
      m_innerThreadId(0),
      m_enterCount   (0),
      m_outerThreadId(0)
{
    SAL_INFO("cppu.affinebridge", "LIFE: AffineBridge::AffineBridge(uno_Environment * pEnv) -> " << this);
}

AffineBridge::~AffineBridge()
{
    SAL_INFO("cppu.affinebridge", "LIFE: AffineBridge::~AffineBridge() -> " << this);

    if (m_pInnerThread && osl::Thread::getCurrentIdentifier() != m_innerThreadId)
    {
        m_message = CB_DONE;
        m_innerCondition.set();

        m_pInnerThread->join();
    }

    m_pInnerThread.reset();

    if (m_pOuterThread)
    {
        m_pOuterThread->join();
    }
}


void AffineBridge::outerDispatch(bool loop)
{
    OSL_ASSERT(m_outerThreadId == osl::Thread::getCurrentIdentifier());
    OSL_ASSERT(m_innerThreadId != m_outerThreadId);

    Msg mm;

    do
    {
        // FIXME: created outer thread must not wait
        // in case of no message
        // note: no message can happen in case newly created
        // outer thread acquire outerMutex after a real outer
        // thread enters outerDispatch!
        m_outerCondition.wait();
        m_outerCondition.reset();

        mm = m_message;

        switch(mm)
        {
        case CB_DONE:
            break;

        case CB_FPOINTER:
        {
            m_pCallee(m_pParam);

            m_message = CB_DONE;
            m_innerCondition.set();
            break;
        }
        default:
            abort();
        }
    }
    while(mm != CB_DONE && loop);
}

void AffineBridge::innerDispatch()
{
    OSL_ASSERT(m_innerThreadId == osl::Thread::getCurrentIdentifier());
    OSL_ASSERT(m_innerThreadId != m_outerThreadId);

    Msg mm;

    do
    {
        m_innerCondition.wait();
        m_innerCondition.reset();

        mm = m_message;

        switch(mm)
        {
        case CB_DONE:
            break;

        case CB_FPOINTER:
        {
            m_pCallee(m_pParam);

            m_message = CB_DONE;
            m_outerCondition.set();
            break;
        }
        default:
            abort();
        }
    }
    while(mm != CB_DONE);
}

void AffineBridge::v_callInto_v(uno_EnvCallee * pCallee, va_list * pParam)
{
    osl::MutexGuard guard(m_outerMutex); // only one thread at a time can call into

    if (m_innerThreadId == 0) // no inner thread yet
    {
        m_pInnerThread.reset(new InnerThread(this));
    }

    bool bResetId = false;
    if (!m_outerThreadId)
    {
        m_outerThreadId = osl::Thread::getCurrentIdentifier();
        bResetId = true;
    }

    m_message = CB_FPOINTER;
    m_pCallee = pCallee;
    m_pParam  = pParam;
    m_innerCondition.set();

    outerDispatch(true);

    if (bResetId)
        m_outerThreadId = 0;
}

void AffineBridge::v_callOut_v(uno_EnvCallee * pCallee, va_list * pParam)
{
    OSL_ASSERT(m_innerThreadId);

    osl::MutexGuard guard(m_innerMutex);

    if (m_outerThreadId == 0) // no outer thread yet
    {
        osl::MutexGuard guard_m_outerMutex(m_outerMutex);

        if (m_outerThreadId == 0)
        {
            if (m_pOuterThread)
            {
                m_pOuterThread->join();
            }

            m_pOuterThread.reset(new OuterThread(this));
        }
    }

    m_message = CB_FPOINTER;
    m_pCallee = pCallee;
    m_pParam  = pParam;
    m_outerCondition.set();

    innerDispatch();
}

void AffineBridge::v_enter()
{
    m_innerMutex.acquire();

    if (!m_enterCount)
        m_innerThreadId = osl::Thread::getCurrentIdentifier();

    OSL_ASSERT(m_innerThreadId == osl::Thread::getCurrentIdentifier());

    ++ m_enterCount;
}

void AffineBridge::v_leave()
{
    OSL_ASSERT(m_innerThreadId == osl::Thread::getCurrentIdentifier());

    -- m_enterCount;
    if (!m_enterCount)
        m_innerThreadId = 0;

    m_innerMutex.release();
}

bool AffineBridge::v_isValid(OUString * pReason)
{
    bool result = m_enterCount > 0;
    if (!result)
        *pReason = "not entered";

    else
    {
        result = m_innerThreadId == osl::Thread::getCurrentIdentifier();

        if (!result)
            *pReason = "wrong thread";
    }

    if (result)
        *pReason = "OK";

    return result;
}

#ifdef DISABLE_DYNLOADING

#define uno_initEnvironment affine_uno_uno_initEnvironment
#define uno_ext_getMapping affine_uno_uno_ext_getMapping

#endif

extern "C" void SAL_DLLPUBLIC_EXPORT uno_initEnvironment(uno_Environment * pEnv) noexcept
{
    cppu::helper::purpenv::Environment_initWithEnterable(pEnv, new AffineBridge());
}

extern "C" void SAL_DLLPUBLIC_EXPORT uno_ext_getMapping(uno_Mapping     ** ppMapping,
                                                        uno_Environment  * pFrom,
                                                        uno_Environment  * pTo )
{
    cppu::helper::purpenv::createMapping(ppMapping, pFrom, pTo);
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
