

Wanderer = function (channels, basis) {
    this.channels = channels;

    this.basis = basis;
    this.velocity = [];
    this.mix = [];
    this.values = [];
    this.lastt = 0.0;

    for (var bi = 0; bi < this.basis.length; bi++) {
        // current velocity through mix space
        this.velocity[bi] = 0.0;
    
        // current mix of indexed primaries
        this.mix[bi] = 0.0;
    }
        
    // current state of the color channels
    //  computed from mix vector and primaries
    for (var ci = 0; ci < this.channels.length; ci++) {
        this.values[ci] = 0.0 / this.channels.length;
        //this.values[ci] = 1.0 / this.channels.length;
    }
    
    // the sum of the velocity elements must be zero
    //  to ensure that the mix is an interpolation!
    // normalize often...
    
    // parameter state (change in X in seconds)
    this.target_speed = 0.7;
};


// renormalize the mix vector so that it is an interpolation
Wanderer.prototype.renormalize_mix = function () {
    // renormalize the mix so it is for sure an interpolation
    // i.e. sum(mix[i]) = 1.0

    var sum = 0;
    var maxi = -1;
    var maxcoef = 0;
    for (var i = 0; i < this.basis.length; i++) {
        var coef = this.mix[i];
        sum += coef;
        if (coef > maxcoef) {
            maxi = i;
            maxcoef = coef;
        }
    }

    //if (sum >= 1.0) return;

    // convert sum to offset
    sum -= 1.0;

    if (sum > 0) {
        var dcoef = sum / this.basis.length;
        for (var i = 0; i < this.basis.length; i++) {
            var coef = this.mix[i];
            if (coef > dcoef) {
                sum -= dcoef;
                coef -= dcoef;
            } else {
                sum -= coef;
                coef = 0;
            }
            this.mix[i] = coef;
        }
    } else if (sum < 0) {
        var dcoef = - sum / this.basis.length;
        for (var i = 0; i < this.basis.length; i++) {
            var coef = this.mix[i];
            if (coef <= 1.0 - dcoef) {
                sum += dcoef;
                coef += dcoef;
            } else {
                sum += 1.0 - coef;
                coef = 1.0;
            }
            this.mix[i] = coef;
        }
    }

    // do something with the remainder?  it will be within [-this.basis.length,indices]
    //  which is not bad as long as we don't overflow somewhere...
};


//////////////////////////////////////////////////////////////////////
//
//  update the this.values vector from the mix vector and
//    the channel color matrix
//
//  update the lamp channel values from this.values
//
Wanderer.prototype.update_values = function () {
    //
    //  multiply mix vector by relevant rows of the primary matrix
    //   to get the lamp values

    // zero the value accumulator
    for (var ci = 0; ci < this.channels.length; ci++) {
        this.values[ci] = 0.0;
    }

    // go through each active primary, weighting it by the
    // mixing coefficient and adding it to the accumulator
    for (var i = 0; i < this.basis.length; i++) {
        var bv = this.basis[i];
        var coef = this.mix[i];

        for (var ci = 0; ci < this.channels.length; ci++) {
            this.values[ci] += bv[ci] * coef;
        }
    }
};

Wanderer.prototype.init = function () {
    var mixamt = 1.0 / this.basis.length;
    for (var i = 0; i < this.basis.length; i++) {
        // "randomize" velocity
        this.velocity[i] = i + 1 - this.basis.length / 2.0;

        this.mix[i] = mixamt;
    }

    this.renormalize_mix();
    this.lastt = new Date();
};


Wanderer.prototype.update_velocity = function () {
    // speed is mag(this.velocity) - computed using L1 norm for speed
    var speed = 0;
    for (var i = 0; i < this.basis.length; i++) {
        if (this.velocity[i] > 0)
            speed += this.velocity[i];
        else 
            speed -= this.velocity[i];
    }

    // scale velocity by target_speed/speed
    var magscale = this.target_speed / speed;
    var vsum = 0;
    for (var i = 0; i < this.basis.length; i++) {
        this.velocity[i] *= magscale;
        vsum += this.velocity[i];
    }

    // recenter the velocity around zero
    var delt = - vsum / this.basis.length;
    if (delt != 0) {
        for (var i = 0; i < this.basis.length; i++)
            this.velocity[i] += delt;
    }
};


//////////////////////////////////////////////////////////////////////
//
//  reflect the velocity vector around a plane x[pi] = 0
//   while still constraining it to the plane sum(v[i]) = 0
//
Wanderer.prototype.bounce = function (pi) {
    var sum = 0;
    for (var i = 0; i < this.basis.length; i++) {
        sum += this.velocity[i];
    }

    // normal to "wall" at x[pi] == 0 is:
    //   nw[i != pi] = 0
    //   nw[pi]      = 1
    // (nw is normalized)

    // normal to the plane that constrains us to interpolations only is:
    //   ni, where ni[i] = 1 / sqrt(N)
    // (ni is normalized)

    // constrain nw to lie in the constrained plane
    //   n = nw - (nw . ni) ni
    // expand nw . ni == ni[pi] == 1 / sqrt(N)
    //   n = nw - ni / sqrt(N)
    // so the vector to reflect around to "bounce" is n:
    //   n[i != pi] = - 1/N
    //   n[pi]      = 1 - 1/N
    // mag^2(n) == 1 - 1/N

    // nn = n / mag(n)
    // reflect v about nn:
    //  v' = v - 2 (v . nn) nn
    // and expand nn:
    //     = v - 2 (v . n) n / mag^2(n)
    //     = v - (2 (v . n) / (1 - 1/N)) n
    //     = v - (2 N (v . n) / (N - 1)) n

    // expand n
    //  v' = v - (2 N (v . nw - v . (nw . ni) ni) / (N - 1)) n
    //     = v - (2 N (v . nw - (nw . ni)(v . ni)) / (N - 1)) n
    // expand nw
    //     = v - (2 N (v[pi] - (1/sqrt(N))(sum(v)/sqrt(N))) / (N - 1)) n
    //     = v - (2 N (v[pi] - sum(v)/N) / (N - 1)) n
    // we know that sum(v) = 0, so
    //     = v - (2 N v[pi] / (N - 1)) n

    // introduce tmp so that:
    //  v' = v - (tmp * N/(N-1)) n
    //int tmp = 2 * this.velocity[pi];

    // expand n again
    //  v' = v - (2 v[pi] N/(N-1) (nw - ni / sqrt(N))
    //     = v - 2 v[pi] N/(N-1) nw - 2 v[pi] N/(N-1) ni / sqrt(N)
    // expand nw[pi]=1  and ni[i]=1/sqrt(N)
    // so v'[i != pi]  = v[i] - 2 v[pi] N/(N-1) 1/sqrt(N) / sqrt(N)
    //                 = v[i] - 2 v[pi] / (N-1)
    //    v'[pi]       = v[i] - 2 v[pi] N/(N-1) - 2 v[pi] / (N-1)
    //                 = v[i] - (2 v[pi] / (N-1)) (N-1)
    //                 = v[i] - 2 v[pi]

    var tmp = this.velocity[pi];
    for (var i = 0; i < this.basis.length; i++) {
        if (i == pi) {
            this.velocity[i] *= -1;
        } else {
            this.velocity[i] += 2 * tmp / (this.basis.length-1);
        }
    }
};

//////////////////////////////////////////////////////////////////////
//
// take a step
//
//  check for violated constraints (mix vector under/overflow)
//  make velocity "bounce" away from violated constraints
//
//  add the current velocity to the mix vector
//
//  dt is in milliseconds
//
Wanderer.prototype.step = function (dt) {
    // add the velocity to the mix.
    //   if that would take the mix outside the [0,1] range,
    //    - reverse that component of the velocity.
    //    - set coef to 0 or 1
    //      - this isn't exactly a bounce, but close enough

//console.log('STEP', this.velocity, dt);
//console.log(' MIX', this.mix);
//console.log('   V', this.values);
    for (var i = 0; i < this.basis.length; i++) {
        var coef = this.mix[i];
        var dcoef = this.velocity[i] * dt;
        var newcoef = 0;

        if (dcoef < 0) {
            if (coef+dcoef < 0) {
                this.bounce(i);
                newcoef = 0.0;
            } else {
                newcoef = coef + dcoef;
            }
        } else {
            // can probably take out this case... constraining
            //  to the plane sum(mix)=1 means no coef can be > 1
            if (coef+dcoef > 1) {
                this.bounce(i);
                newcoef = 1.0;
            } else {
                newcoef = coef + dcoef;
            }
        }

        this.mix[i] = newcoef;
    }

    // renormalize the velocity (to be centered around 0)
    this.update_velocity();

    // and the mix (must sum to 1)
    this.renormalize_mix();
};
