#include "fmod_settings.h"

#ifdef FMOD_SUPPORT_NONBLOCKING

#include "fmod_async.h"
#include "fmod_dspi.h"
#include "fmod_soundi.h"
#include "fmod_systemi.h"

namespace FMOD
{

LinkedListNode AsyncThread::gAsyncHead;


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
void asyncThreadFunc(void *data)
{
    AsyncThread *asyncThread = (AsyncThread *)data;
    asyncThread->threadFunc();
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
AsyncThread::AsyncThread()
{
    mCrit         = 0;
    mThreadActive = false;
    mBusy         = false;
    mDone         = false;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::shutDown()
{
    if (FMOD::gGlobal->gAsyncCrit)
    {
        AsyncThread *asyncthread;
        LinkedListNode *current, *next;

        FMOD_OS_CriticalSection_Enter(FMOD::gGlobal->gAsyncCrit);
        {
            current = gAsyncHead.getNext();
            while (current != &gAsyncHead)
            {
                next = current->getNext();

                asyncthread = SAFE_CAST(AsyncThread, current);
                asyncthread->reallyRelease();

                current = next;
            }
        }
        FMOD_OS_CriticalSection_Leave(FMOD::gGlobal->gAsyncCrit);
    }

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::addCallback(FMOD_ASYNC_CALLBACK callback, AsyncThread **asyncthread)
{
    FMOD_RESULT result;

    if (asyncthread)
    {
        *asyncthread = 0;
    }

    LinkedListNode *node = (LinkedListNode *)FMOD_Object_Alloc(LinkedListNode);
    if (!node)
    {
        return FMOD_ERR_MEMORY;
    }

    node->setData((void *)callback);

    /*
        Make sure there's a thread for us
    */
    result = getAsyncThread(0);
    if (result != FMOD_OK)
    {
        return result;
    }

    LinkedListNode *threadnode = gAsyncHead.getNext();
    if (threadnode != &gAsyncHead)
    {
        AsyncThread *thread = SAFE_CAST(AsyncThread, threadnode);
        FMOD_OS_CriticalSection_Enter(FMOD::gGlobal->gAsyncCrit);
        {
            node->addBefore(&thread->mCallbackHead);
        }
        FMOD_OS_CriticalSection_Leave(FMOD::gGlobal->gAsyncCrit);

        if (asyncthread)
        {
            *asyncthread = thread;
        }
    }
    else
    {
        return FMOD_ERR_INTERNAL;
    }

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::removeCallback(FMOD_ASYNC_CALLBACK callback)
{
    LinkedListNode *threadnode = gAsyncHead.getNext();

    if (threadnode != &gAsyncHead)
    {
        AsyncThread *asyncthread = SAFE_CAST(AsyncThread, threadnode);

        FMOD_OS_CriticalSection_Enter(FMOD::gGlobal->gAsyncCrit);

        LinkedListNode *callbacknode = asyncthread->mCallbackHead.getNext();
        for (; callbacknode != &asyncthread->mCallbackHead; callbacknode = callbacknode->getNext())
        {
            if (callbacknode->getData() == (void *)callback)
            {
                callbacknode->removeNode();
                FMOD_Memory_Free(callbacknode);
                break;
            }
        }

        FMOD_OS_CriticalSection_Leave(FMOD::gGlobal->gAsyncCrit);
    }
    else
    {
        return FMOD_ERR_INTERNAL;
    }

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::init(bool owned, SystemI *system)
{
    FMOD_RESULT result;

    mOwned = owned;

    result = FMOD_OS_CriticalSection_Create(&mCrit);
    if (result != FMOD_OK)
    {
        return result;
    }

    result = mThread.initThread("FMOD thread for FMOD_NONBLOCKING", asyncThreadFunc, this, ASYNC_THREADPRIORITY, 0, ASYNC_STACKSIZE, true, 0, system);
    if (result != FMOD_OK)
    {
        return result;
    }

    mThreadActive = true;

    FMOD_OS_CriticalSection_Enter(FMOD::gGlobal->gAsyncCrit);
    addBefore(&gAsyncHead);
    FMOD_OS_CriticalSection_Leave(FMOD::gGlobal->gAsyncCrit);

    FLOG((FMOD_DEBUG_USER_ANDREW, __FILE__, __LINE__, "AsyncThread::init", "created thread for %p %s\n", this, mOwned ? "(owned)" : ""));

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::release()
{
    if (mOwned)
    {
        if (mHead.getNext() != &mHead)
        {
            FLOG((FMOD_DEBUG_USER_ANDREW, __FILE__, __LINE__, "AsyncThread::release", "%p queue not empty\n", this));
        }

        mDone = true;
    }

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::reallyRelease()
{
    FMOD_OS_CriticalSection_Enter(mCrit);
    {
        if (mHead.getNext() != &mHead)
        {
            FLOG((FMOD_DEBUG_USER_ANDREW, __FILE__, __LINE__, "AsyncThread::reallyRelease", "%p queue not empty\n", this));
        }
        if (mBusy)
        {
            FLOG((FMOD_DEBUG_USER_ANDREW, __FILE__, __LINE__, "AsyncThread::reallyRelease", "%p still busy\n", this));
        }

        LinkedListNode *next, *current = mCallbackHead.getNext();
        while (current != &mCallbackHead)
        {
            next = current->getNext();
            current->removeNode();
            FMOD_Memory_Free(current);
            current = next;
        }
    }
    FMOD_OS_CriticalSection_Leave(mCrit);

    // FMOD_OS_CriticalSection_Enter(FMOD::gGlobal->gAsyncCrit);
    removeNode();
    // FMOD_OS_CriticalSection_Leave(FMOD::gGlobal->gAsyncCrit);

    mThreadActive = false;
    mThread.closeThread();

    if (mCrit)
    {
        FMOD_OS_CriticalSection_Free(mCrit);
    }

    FLOG((FMOD_DEBUG_USER_ANDREW, __FILE__, __LINE__, "AsyncThread::reallyRelease", "released thread for %p %s\n", this, mOwned ? "(owned)" : ""));

    FMOD_Memory_Free(this);

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::wakeupThread()
{
    return mThread.wakeupThread();
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::update()
{
    if (FMOD::gGlobal->gAsyncCrit)
    {
        AsyncThread *asyncthread;
        LinkedListNode *current, *next;

        FMOD_OS_CriticalSection_Enter(FMOD::gGlobal->gAsyncCrit);
        {
            current = gAsyncHead.getNext();
            while (current != &gAsyncHead)
            {
                next = current->getNext();

                asyncthread = SAFE_CAST(AsyncThread, current);
                if (asyncthread->mDone)
                {
                    asyncthread->reallyRelease();
                }

                current = next;
            }
        }
        FMOD_OS_CriticalSection_Leave(FMOD::gGlobal->gAsyncCrit);
    }

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::threadFunc()
{
    FMOD_RESULT     result = FMOD_OK;
    SoundI         *sound = 0;
    SystemI        *system;
    LinkedListNode *current;

    if (mThreadActive)
    {
        FMOD_OS_CriticalSection_Enter(mCrit);
        {
            current = mHead.getNext();
            if (current != &mHead)
            {
                sound  = (SoundI *)current->getData();
                current->removeNode();
                mBusy = true;
            }
            else
            {
                current = 0;
            }
        }
        FMOD_OS_CriticalSection_Leave(mCrit);

        if (sound)
        {
            FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "AsyncThread::threadFunc", "Starting Asynchronous operation on sound %p\n", sound));
            FLOG_INDENT(4);

            system = sound->mSystem;

            if (sound->mOpenState == FMOD_OPENSTATE_LOADING)
            {
                if (sound->mMode & (FMOD_OPENMEMORY | FMOD_OPENMEMORY_POINT))
                {
                    result = system->createSoundInternal((const char *)sound->mAsyncData->mNameData, sound->mMode, sound->mAsyncData->mBufferSize, sound->mAsyncData->mBufferSizeType, sound->mAsyncData->mExInfoExists ? &sound->mAsyncData->mExInfo : 0, true, &sound);
                }
                else
                {
                    result = system->createSoundInternal(sound->mAsyncData->mName, sound->mMode, sound->mAsyncData->mBufferSize, sound->mAsyncData->mBufferSizeType, sound->mAsyncData->mExInfoExists ? &sound->mAsyncData->mExInfo : 0, true, &sound);
                }
            }
#ifdef FMOD_SUPPORT_STREAMING
#ifdef FMOD_SUPPORT_NONBLOCKSETPOS
            else if (sound->mOpenState == FMOD_OPENSTATE_SETPOSITION)
            {
                Stream *stream = SAFE_CAST(Stream, sound);

                /*
                    Block here and wait for stream thread to stop doing stuff.
                */
                while (!(stream->mFlags & (FMOD_SOUND_FLAG_SETPOS_SAFE | FMOD_SOUND_FLAG_THREADFINISHED)))
                {
                    FMOD_OS_Time_Sleep(10);
                }

                if (!(stream->mFlags & FMOD_SOUND_FLAG_THREADFINISHED))
                {
                    result = stream->mChannel->setPositionEx(sound->mAsyncData->mPosition, sound->mAsyncData->mPositionType, true);
                    if (result == FMOD_OK)
                    {
                        stream->mChannel->mFlags &= ~CHANNELREAL_FLAG_PAUSEDFORSETPOS;

                	    FMOD_OS_CriticalSection_Enter(sound->mSystem->mStreamRealchanCrit);
                        if (stream->mChannel->mRealChannel[0])
                        {
                            stream->mChannel->setPaused(stream->mChannel->mFlags & CHANNELREAL_FLAG_PAUSED ? true : false);
                        }
                	    FMOD_OS_CriticalSection_Leave(sound->mSystem->mStreamRealchanCrit);
                    }
                    else if (result == FMOD_ERR_INVALID_HANDLE)
                    {
                        result = FMOD_OK;
                    }
                }
            }
#endif
            else if (sound->mOpenState == FMOD_OPENSTATE_SEEKING)
            {
                Stream *stream = SAFE_CAST(Stream, sound);
                
                if (stream->mSubSoundList)
                {
                    result = FMOD_OK;
                }
                else
                {
                    result = stream->updateSubSound(stream->mSubSoundIndex, false);
                }
                
                if (result == FMOD_OK)
                {
                    result = stream->setPosition(0, FMOD_TIMEUNIT_PCM);                               
                    FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "AsyncThread::threadFunc", "done setposition\n"));
                    if (result == FMOD_OK)
                    {
                        result = stream->flush();
                        if (result == FMOD_OK)
                        {
                            FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "AsyncThread::threadFunc", "done flush\n"));
                        }
                        else
                        {
                            FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "AsyncThread::threadFunc", "stream->flush returned %d\n", result));
                        }
                    }
                    else
                    {
                        FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "AsyncThread::threadFunc", "stream->setPosition returned %d\n", result));
                    }
                }
            }
#endif
            else
            {
                FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "AsyncThread::threadFunc", "AsyncThread::threadFunc: unexpected mOpenState (%d).  Result = %d\n", sound->mOpenState, result));
            }

            sound->mAsyncData->mResult = result;
            sound->mFlags             |= FMOD_SOUND_FLAG_DONOTRELEASE;                                               /* Dissalow release... */
            sound->mOpenState          = (result == FMOD_OK) ? FMOD_OPENSTATE_READY : FMOD_OPENSTATE_ERROR;     /* .. but allow everything else. */
            if (sound->mSubSoundParent)
            {
                sound->mSubSoundParent->mOpenState = sound->mOpenState;
            }
            if (sound->mSubSoundShared)
            {
                sound->mSubSoundShared->mOpenState = sound->mOpenState;
            }
            mBusy                      = false;

            if (sound->mAsyncData->mExInfoExists)
            {
                if (sound->mAsyncData->mExInfo.nonblockcallback)
                {
                    sound->mUserData = sound->mAsyncData->mExInfo.userdata;

                    sound->mAsyncData->mExInfo.nonblockcallback((FMOD_SOUND *)sound, result);
                }
            }

            sound->mFlags &= ~FMOD_SOUND_FLAG_DONOTRELEASE;

            /*
                Release this thread if it's owned
            */
            release();

            FLOG_INDENT(-4);
            FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "AsyncThread::threadFunc", "Finished Asynchronous operation on sound %p\n", sound));
        }

        /*
            Call any callbacks that have been added
        */
        LinkedListNode *node;

        FMOD_OS_CriticalSection_Enter(mCrit);
        {
            node = mCallbackHead.getNext();
        }
        FMOD_OS_CriticalSection_Leave(mCrit);

        while (node != &mCallbackHead)
        {
            FMOD_ASYNC_CALLBACK callback = (FMOD_ASYNC_CALLBACK)node->getData();

            result = callback();
            if (result != FMOD_OK)
            {
                return result;
            }

            FMOD_OS_CriticalSection_Enter(mCrit);
            {
                node = node->getNext();
            }
            FMOD_OS_CriticalSection_Leave(mCrit);
        }

    }

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT AsyncThread::getAsyncThread(SoundI *sound)
{
    FMOD_RESULT     result;
    LinkedListNode *current;
    bool            found, owned;
    AsyncThread    *asyncthread = 0;

    owned = false;
    found = false;

    FMOD_OS_CriticalSection_Enter(FMOD::gGlobal->gAsyncCrit);
    {
        current = gAsyncHead.getNext();
        if (current != &gAsyncHead)
        {
            asyncthread = SAFE_CAST(AsyncThread, current);

            FMOD_OS_CriticalSection_Enter(asyncthread->mCrit);
            {
                if (0) //(asyncthread->mHead.getNext() != &asyncthread->mHead) || asyncthread->mBusy || asyncthread->mDone)
                {
                    owned = true;       /* The first asyncthread is or will be busy so make a new one */
                }
                else
                {
                    found = true;       /* The first asyncthread isn't doing anything so use it */
                }
            }
            FMOD_OS_CriticalSection_Leave(asyncthread->mCrit);
        }
    }
    FMOD_OS_CriticalSection_Leave(FMOD::gGlobal->gAsyncCrit);

    if (!found)
    {
        asyncthread = FMOD_Object_Alloc(AsyncThread);
        if (!asyncthread)
        {
            return FMOD_ERR_MEMORY;
        }

        result = asyncthread->init(owned, sound ? sound->mSystem : 0);
        if (result != FMOD_OK)
        {
            return result;
        }
    }

    if (sound)
    {
        if (sound->mAsyncData->mThread)
        {
            FLOG((FMOD_DEBUG_USER_ANDREW, __FILE__, __LINE__, "AsyncThread::getAsyncThread", "sound->mAsyncData->Thread not null == %p\n", sound->mAsyncData->mThread));
        }

        sound->mAsyncData->mThread = asyncthread;
    }

    return FMOD_OK;
}


}

#endif
