// AC sound scheduler, manages available sound sources.
// It keeps a set of reserved sources for each priority level to avoid running out of sources by floods of low-priority sounds.
// Under load it uses priority and distance information to reassign its resources.

#include "cube.h"

#define DEBUGCOND (audiodebug==1)

VARP(soundschedpriorityscore, 0, 100, 1000);
VARP(soundscheddistancescore, 0, 5, 1000);
VARP(soundschedoldbonus, 0, 100, 1000);
VARP(soundschedreserve, 0, 2, 100);

sourcescheduler *sourcescheduler::inst;

sourcescheduler::sourcescheduler()
{
}

sourcescheduler &sourcescheduler::instance()
{
    if(inst==NULL) inst = new sourcescheduler();
    return *inst;
}

void sourcescheduler::init(int numsoundchannels)
{
    this->numsoundchannels = numsoundchannels;
    int newchannels = numsoundchannels - sources.length();
    if(newchannels < 0)
    {
        loopv(sources)
        {
            source *src = sources[i];
            if(src->locked) continue;
            sources.remove(i--);
            delete src;
            if(sources.length() <= numsoundchannels) break;
        }
    }
    else loopi(newchannels)
    {
        source *src = new source();
        if(src->valid) sources.add(src);
        else
        {
            DELETEP(src);
            break;
        }
    }
}

void sourcescheduler::reset()
{
    loopv(sources) sources[i]->reset();
    sources.deletecontents();
}

// returns a free sound source (channel)
// consuming code must call sourcescheduler::releasesource() after use

source *sourcescheduler::newsource(int priority, const vec &o)
{
    if(!sources.length()) 
    {
        DEBUG("empty source collection");
        return NULL;
    }

    source *src = NULL;

    // reserve some sources for sounds of higher priority
    int reserved = (SP_HIGHEST-priority)*soundschedreserve;
    DEBUGVAR(reserved);
    
    // search unused source
    loopv(sources) 
    {
        if(!sources[i]->locked && reserved--<=0)
        {
            src = sources[i];
            DEBUGVAR(src);
            break;
        }
    }

    if(!src) 
    {
        DEBUG("no empty source found");

        // low priority sounds can't replace others
        if(SP_LOW==priority)
        {
            DEBUG("low prio sound aborted");
            return NULL;
        }

        // try replacing a used source
        // score our sound
        const float dist = o.iszero() ? 0.0f : camera1->o.dist(o);
        const float score = (priority*soundschedpriorityscore) - (dist*soundscheddistancescore);

        // score other sounds
        float worstscore = 0.0f;
        source *worstsource = NULL;

        loopv(sources)
        {
            source *s = sources[i];
            if(s->priority==SP_HIGHEST) continue; // highest priority sounds can't be replaced
            
            const vec & otherpos = s->position();
            float otherdist = otherpos.iszero() ? 0.0f : camera1->o.dist(otherpos);
            float otherscore = (s->priority*soundschedpriorityscore) - (otherdist*soundscheddistancescore) + soundschedoldbonus;
            if(!worstsource || otherscore < worstscore)
            {
                worstsource = s;
                worstscore = otherscore;
            }
        }

        // pick worst source and replace it
        if(worstsource && score>worstscore)
        {
            src = worstsource;
            src->onreassign(); // inform previous owner about the take-over
            DEBUG("replaced sound of same prio");
        }
    }

    if(!src) 
    {
        DEBUG("sound aborted, no channel takeover possible");
        return NULL;
    }

    src->reset();       // default settings
    src->lock();        // exclusive lock
    src->priority = priority;
    return src;
}

// give source back to the pool
void sourcescheduler::releasesource(source *src)
{
    ASSERT(src);
    ASSERT(src->locked); // detect double release

    if(!src) return;
    
	DEBUG("unlocking source");
	
	src->unlock();

    if(sources.length() > numsoundchannels)
    {
        sources.removeobj(src);
        delete src;
		DEBUG("source deleted");
    }
}


