//
//  Lamp.h
//  Wbl3001
//
//  Created by david van brink on 3/21/13.
//
//

#ifndef Wbl3001_Lamp_h
#define Wbl3001_Lamp_h

class Lamp
{
    Om *om;
    u32 tag;     // rgb lamp on front of device
    u32 rearTag; // on-off LED on back of device
    u32 pitch;
    bool isRelease; // if true, the end of this "glide" signifies the lamp is done.
    
    f32 brightnessStep;
    
public:
    u32 hue;
    f32 brightness;
    f32 brightnessTarget;
    u32 boundRef;
    
    Lamp(Om *_om, u32 _tag, u32 _rearTag)
    {
        this->om = _om;
        this->tag = _tag;
        this->rearTag = _rearTag;
        this->boundRef = 0;
    }
    
    
    //     step/batch = samples/batch * seconds/sample * milliseconds/second * step/totalMilliseconds
    
    
    void glideTo(u32 _brightnessTarget, u32 milliseconds)
    {
        this->brightnessTarget = _brightnessTarget;
        
        // but! only glide darker. thats the trick.
        if(this->brightnessTarget > this->brightness)
            milliseconds = 0;
        
        if(milliseconds)
        {
            this->brightnessStep = (double)(this->brightnessTarget - this->brightness)
            * 8.0
            / (double)milliseconds  // total time
            * 64.0            // samples/batch
            * 1000.0          // milliseconds/second
            / this->om->getSystemSampleRate();
        }
        else
        {
            this->brightness = this->brightnessTarget;
        }
    }
    void set(u32 _hue, f32 _brightness) {
        
        this->hue = _hue;
        this->brightness = _brightness;
        
        // slight adjustments to render, but not model
#define MINB 1
        if(_brightness < MINB)
            _brightness = MINB;
        
        int frame = (hue * BRIGHTSTEPS) + _brightness;
        f32 lampVal = (float)frame / (HUESTEPS * BRIGHTSTEPS - 1);
        
        om->setNumberByPropertyTag(this->tag,lampVal);
        // and simplified back-panel view.
        om->setBooleanByPropertyTag(this->rearTag, brightness > BRIGHTSTEPS / 3);
    }
    
    bool glide()
    {
        f32 nextBrightness = this->brightness + this->brightnessStep;
        bool gotThere = this->brightnessStep > 0
        ? (nextBrightness >= this->brightnessTarget)
        : (nextBrightness <= this->brightnessTarget);
        if(gotThere)
        {
            nextBrightness = this->brightnessTarget;
        }
        this->set(this->hue,nextBrightness);
        return gotThere;
    }
    
    
};

template <int S>
class LampCollection
{
    List<S,Lamp *> lamps;
    List<S,Lamp *> activeLamps;
    Map<S,u32, Lamp *> bound;
    List<S,Lamp *> toBeRemoved; // reused in each render batch.
    
    u32 firstTag;
    u32 firstRearTag;
    u32 count;
    
    u32 walker; // to stir up the theft when binding.
    
public:
    LampCollection()
    {
        this->walker = 0;
    }

    ~LampCollection()
    {
        for(int i = 0; i < this->lamps.getSize(); i++)
        {
            Lamp *aLamp = this->lamps.getAt(i);
            delete aLamp;
        }
        this->lamps.clear();
    }

    void init(Om *om, u32 _firstTag, u32 _firstRearTag)
    {
        this->firstTag = _firstTag;
        this->firstRearTag = _firstRearTag;
        this->count = S;
        for(u32 i = 0; i < S; i++)
        {
            Lamp *aLamp = new Lamp(om, firstTag + i, this->firstRearTag ? this->firstRearTag + i : 0);
            this->lamps.add(aLamp);
        }
    }
    
    static u32 pinBrightness(u32 brightness)
    {
        if(brightness >= BRIGHTSTEPS)
        {
            if(brightness & 0x80000000)
                brightness = 0;
            else
                brightness = BRIGHTSTEPS - 1;
        }
        return brightness;
    }

    // force a lamp to a certain value immediately
    void set(u32 tag, u32 hue, u32 brightness)
    {
        brightness = pinBrightness(brightness);
        u32 ix = tag - this->firstTag;
        if(ix >= count)
            return;
        Lamp *lamp = this->lamps.getAt(ix);
        lamp->set(hue,brightness);
        this->activeLamps.remove(lamp); // fixed value? no longer in motion
    }

#define SAME_HUE 0x8088

    void glideTo(u32 tag, u32 hue, u32 brightness, u32 milliseconds)
    {
        brightness = pinBrightness(brightness);
        u32 ix = tag - this->firstTag;
        if(ix >= count)
            return;
        Lamp *lamp = this->lamps.getAt(ix);
        if(hue != SAME_HUE)
            lamp->hue = hue;
        lamp->glideTo(brightness,milliseconds);
        this->activeLamps.add(lamp); // fixed value? no longer in motion
    }

    // tell a lamp to glide to a certain value over time
    void glideTo(u32 tag, u32 brightness, u32 milliseconds)
    {
        this->glideTo(tag,SAME_HUE,brightness,milliseconds);
    }
    
    // "by ref" on init means, find an available lamp and bind it to the pitch ref, or fail
    void setByRef(u32 ref, u32 hue, u32 brightness)
    {
        // first, try already-bound-to-this-ref
        // second, unbound
        // third, bound with target brightness of zero.
        Lamp *lamp = this->bound.get(ref);
        if(lamp)
            goto gotLamp;
        
        this->walker++;
        for(u32 ix = 0; ix < this->count; ix++)
        {
            lamp = this->lamps.getAt((this->walker + ix) % count);
            if(lamp->boundRef == 0)
                goto gotLamp;
        }
        
        for(u32 ix = 0; ix < this->count; ix++)
        {
            lamp = this->lamps.getAt((this->walker + ix) % count);
            if(lamp->brightnessTarget == 0)
                goto gotLamp;
        }
        
        // no sale. (Could fourth -- just take one)
        return;
        
    gotLamp:
        this->bound.clear(lamp->boundRef);
        this->bound.put(ref,lamp);
        lamp->boundRef = ref;
        lamp->set(hue, brightness);
        
        return;
    }
    
    void glideToByRef(u32 ref, u32 brightness, u32 milliseconds)
    {
        Lamp *lamp = this->bound.get(ref);
        if(lamp)
        {
            lamp->glideTo(brightness, milliseconds);
            this->activeLamps.add(lamp); // fixed value? no longer in motion
        }
    }

    void darkenAll(u32 milliseconds)
    {
        for(u32 ix = 0; ix < this->count; ix++)
        {
            this->glideTo(ix + this->firstTag,0,milliseconds);
        }
    }
    
    void copy(u32 refDst, u32 refSrc)
    {
        Lamp *lampDst = this->lamps.getAt(refDst - this->firstTag);
        Lamp *lampSrc = this->lamps.getAt(refSrc - this->firstTag);
        if(lampDst->brightness == 0 && lampSrc->brightness == 0)
            return;
        if(lampSrc->brightness > BRIGHTSTEPS - 3)
            this->set(refDst,lampSrc->hue,lampSrc->brightness);
        else
            this->glideTo(refDst,0,150);
    }
    
    // update all active lamps, as needed.
    void glide()
    {
        if(this->activeLamps.size == 0)
            return;
        this->toBeRemoved.clear();
        
        for(int ix = 0; ix < activeLamps.getSize(); ix++)
        {
            Lamp *lamp = this->activeLamps.getAt(ix);
            bool gotThere = lamp->glide();
            if(gotThere)
                this->toBeRemoved.add(lamp);
        }
        for(int ix = 0; ix < this->toBeRemoved.getSize(); ix++)
        {
            Lamp *lamp = this->toBeRemoved.getAt(ix);
            this->activeLamps.remove(lamp);
            
            // bound lamp that we're done with?
            // boundRef public is odd but enh.
            if(lamp->brightnessTarget == 0 && lamp->boundRef)
                lamp->boundRef = 0;
        }
    }
};


#endif
