// (c) Sam Gratrix.


// ==========================================================================================================
// Compute the greateast common divisor of two integers. I hammer this function so hope hashing will speed
// it up.

var gcd_hash = new Array();
var gcd_size = 0;
var gcd_succ = 0;

function gcd(x, y)
{
  x = parseInt(x);
  y = parseInt(y);

  if(x < y) { var z = x; x = y; y = z; }

  var key = new Array(x, y);

  if( typeof(gcd_hash[key]) == 'undefined' )
  {
    gcd_size++;

    while( y != 0 )
    {
      var t = y;

      y = x % y;

      x = t;
    }

    gcd_hash[key] = x;

    return x;
  }
  else
  {
    gcd_succ++;

    return gcd_hash[key];
  }
}

function lcm(x ,y)
{
  var g = gcd(x, y);

  return ((x / g) * y);
}


// ==========================================================================================================
// Combinations Generalized: The number of ways Sum_i r_i samples can be partitioned into subsets of type i
// with size r_i. Result: (Sum_i r_i)! / Prod_i(r_i!). This function ain't going to be quick, but should not
// internally overflow if the result does not overflow.

var combs_hash = new Array();
var combs_size = 0;
var combs_succ = 0;

function combs(r)
{
  if(typeof(combs_hash[r]) == 'undefined' )
  {
    var a = 0;
    var q = 0;
    var p = new Array();

    for(var i=0; i<r.length; i++)
    {
      r[i] = parseInt(r[i]);

      a += r[i];

      for(var j=2; j<=r[i]; j++)
      {
        p[q++] = j;
      }
    }

    var x = 1;

    for(var n=a; n>1; n--)
    {
      var m = n;

      for(var j=0; j<p.length; j++)
      {
        if(p[j] == 1) continue;

        var g = gcd(m, p[j]);

        if(g > 1)
        {
          m    /= g;
          p[j] /= g;
        }

        if(m == 1) break;
      }

      x *= m;
    }

    combs_hash[r] = x;
    combs_size++;

    return x;
  }
  else
  {
    combs_succ++;

    return combs_hash[r];
  }
}


// ==========================================================================================================
// Hypergeometric Generalized: A population of size Sim_i R_i exhibits a partitioning into subsets of type i
// with size R_i. A sample of size Sum_i r_i is taken without replacement. It is desired that the sample
// exhibits a partitioning into subsets of type i with size r_i. This will compute the number of such ways.
// Result: Prod_i combs(R_i-r_i, r_i). Call with a 2D array.

function hyper(p)
{
    var x = 1;

    for(var i=0; i<p.length; i++) x *= combs(p[i]);

    return x;
}

function hyper_row_sum(p)
{
    x = new Array();

    for(var i=0; i<p.length; i++) x[i] = 0;

    for(var i=0; i<p.length; i++) for(var j=0; j<p[i].length; j++) x[i] += p[i][j];

    return combs(x);
}

function hyper_col_sum(p)
{
    x = new Array();

    for(var j=0; j<p[0].length; j++) x[j] = 0;

    for(var i=0; i<p.length; i++) for(var j=0; j<p[i].length; j++) x[j] += p[i][j];

    return combs(x);
}


// ==========================================================================================================
// Contingency table.

function Contingency(contingency)
{
    this.contingency = contingency;

    this.dim1   = this.contingency.length;
    this.dim2   = this.contingency[0].length;

    this.combs  = 0;
    this.combs1 = 0;
    this.combs2 = 0;
}

Contingency.prototype.compute = function()
{
    this.combs  = hyper(this.contingency);
    this.combs1 = hyper_row_sum(this.contingency);
    this.combs2 = hyper_col_sum(this.contingency);
}

Contingency.prototype.display = function()
{
    var html = '<table cellspacing="0" cellpadding="0" border="0" bgcolor="#FFFFFF" class="par2">';

    for(var r=0; r<this.dim2; r++)
    {
        html += '<tr>';

        for(var c=0; c<this.dim1; c++)
        {
            html += '<td align="right">' + this.contingency[c][r] + '</td>';
        }

        html += '</tr>';
    }

    html += '</table>';

    return html;
}


// ==========================================================================================================
// A lotto match is defined by an array of contingency tables.

function LottoMatch(name, guaranteed, split, contingencies)
{
    this.name          = name;
    this.guaranteed    = guaranteed;
    this.split         = split;
    this.contingencies = contingencies;

    this.name_exact    = this.name;

    this.wins          = 0;
    this.draws         = 0;
    this.tickets       = 0;
    this.prob          = new Rational(0,1);
    this.post_fund     = new Rational(0,1);
    this.post_prize    = new Rational(0,1);
    this.post_payout   = new Rational(0,1);
    this.fair_prize    = new Rational(0,1);
    this.fair_payout   = new Rational(0,1);

    this.group         = this.contingencies[0].length + '-' + this.contingencies[0][0].dim1;  // just for menu icons
}

LottoMatch.prototype.compute1 = function(fee, payout, ways)
{
    this.wins    = 0;
    this.draws   = 0;
    this.tickets = 0;

    for(var j=0; j<this.contingencies.length; j++)
    {
        var wins    = 1;
        var draws   = 1;
        var tickets = 1;

        for(var i=0; i<this.contingencies[j].length; i++)
        {
            this.contingencies[j][i].compute();

            wins    *= this.contingencies[j][i].combs;
            draws   *= this.contingencies[j][i].combs1;
            tickets *= this.contingencies[j][i].combs2;
        }

        this.wins   += wins;
        this.draws   = draws;    // do something here
        this.tickets = tickets;
    }

    this.prob.set_f(this.wins, this.tickets);

    this.fair_payout.set_r(payout);
    this.fair_payout.div_i(ways);

    this.fair_prize.set_r(this.fair_payout);
    this.fair_prize.div_r(this.prob);
    this.fair_prize.mul_r(fee);
}

LottoMatch.prototype.compute2 = function(fee, pool, norm)
{
    if(this.split)
    {
        this.post_fund.set_f(this.split, norm);

        this.post_prize.set_r(pool);
        this.post_prize.mul_r(this.post_fund);
        this.post_prize.div_i(this.wins);
    }
    else
    {
        this.post_prize.set_r(this.guaranteed);
    }

    this.post_payout.set_r(this.post_prize);
    this.post_payout.mul_r(this.prob);
    this.post_payout.div_r(fee);
}

LottoMatch.prototype.display = function(exact)
{
    var html = '';

    html += '<tr bgcolor="#ffffff">';

    html += '<th align="left"  >' + (exact ? this.name_exact : this.name) +'</th>';

    html += '<td align="center"><table cellspacing=0 cellpadding=0 border=0><tr>';

    for(var j=0; j<this.contingencies.length; j++)
    {
        if(j) html += '<td align="center">+</td>'

        html += '<td align="center"><table cellspacing=1 cellpadding=0 border=0 bgcolor="#000066" class="par1"><tr>';

        for(var i=0; i<this.contingencies[j].length; i++)
        {
            html += '<td bgcolor="#FFFFFF">' + this.contingencies[j][i].display() + '</td>';
        }

        html += '</tr></table></td>';
    }

    html += '</tr></table></td>';

    if(exact)
    {
        html += '<td align="right" >' + this.wins           + '</td>';
        html += '<td align="center">' + this.prob.display() + '</td>';

        if(this.post_payout.n == 0)
        {
            html += '<td align="center">--</td>';
            html += '<td align="center">--</td>';
            html += '<td align="center">--</td>';
        }
        else
        {
            html += '<td align="center">' + (this.split ? this.post_fund.display() : 'Fixed') + '</td>';
            html += '<td align="center">' + this.post_prize.display()  + '</td>';
            html += '<td align="center">' + this.post_payout.display() + '</td>';
        }

        html += '<td align="center">' + this.fair_prize.display()  + '</td>';
        html += '<td align="center">' + this.fair_payout.display() + '</td>';
    }
    else
    {
        html += '<td align="right" >' + format(this.wins) + '</td>';
        html += '<td align="left"  >' +   odds(this.prob) + '</td>';
        html += '<td align="center">' +  flips(this.prob) + '</td>';

        if(this.post_payout.n == 0)
        {
            html += '<td align="center">--</td>';
            html += '<td align="center">--</td>';
            html += '<td align="center">--</td>';
        }
        else
        {
            html += '<td align="center">' + (this.split ? twodp(this.post_fund) : 'Fixed') + '</td>';
            html += '<td align="right" >' +  money(this.post_prize)  + '</td>';
            html += '<td align="center">' +  twodp(this.post_payout) + '</td>';
        }

        html += '<td align="right" >' +  money(this.fair_prize)  + '</td>';
        html += '<td align="center">' +  twodp(this.fair_payout) + '</td>';
    }

    html += '</tr>';

    return html;
}


// ==========================================================================================================
// A lotto total is defined by an array of lotto matches. It represents winner and loser.

function LottoTotal(winner)
{
    this.winner      = winner;

    this.wins        = 0;
    this.tickets     = 0;
    this.prob        = new Rational(0,1);
    this.payout      = new Rational(0,1);  // it seems you get the same
    this.prize       = new Rational(0,1);  // for post and fair results
}

LottoTotal.prototype.compute = function(wins, tickets, fee, payout)
{
    this.wins    = wins;
    this.tickets = tickets;

    this.prob.set_f(this.wins, this.tickets);

    if(this.winner)
    {
        this.payout.set_r(payout);

        this.prize.set_r(this.payout);
        this.prize.div_r(this.prob);
        this.prize.mul_r(fee);
    }
}

LottoTotal.prototype.display = function(exact)
{
    var html = '';

    html += '<tr bgcolor="#ffffff">';
    html += '<th align="left">' + (this.winner ? 'Winner' : 'Loser') + '</th>';
    html += '<td align="center">&nbsp;<br>&nbsp;</td>';

    if(exact)
    {
        html += '<td align="right" >' + this.wins           + '</td>';
        html += '<td align="center">' + this.prob.display() + '</td>';
        html += '<td align="center">&nbsp;</td>';

        if(this.winner)
        {
            html += '<td align="center">' + this.prize.display()  + '</td>';
            html += '<td align="center">' + this.payout.display() + '</td>';
            html += '<td align="center">' + this.prize.display()  + '</td>';
            html += '<td align="center">' + this.payout.display() + '</td>';
        }
        else
        {
            html += '<td align="center">&nbsp;</td><td align="center">&nbsp;</td>';
            html += '<td align="center">&nbsp;</td><td align="center">&nbsp;</td>';
        }
    }
    else
    {
        html += '<td align="right">' + format(this.wins) + '</td>';

        if(this.winner)
        {
            html += '<td align="left"  >' +  odds(this.prob)   + '</td>';
            html += '<td align="center">' + flips(this.prob)   + '</td>';
            html += '<td align="center">&nbsp;</td>';
            html += '<td align="right" >' + money(this.prize)  + '</td>';
            html += '<td align="center">' + twodp(this.payout) + '</td>';
            html += '<td align="right" >' + money(this.prize)  + '</td>';
            html += '<td align="center">' + twodp(this.payout) + '</td>';
        }
        else
        {
            html += '<td align="left"  >&nbsp;</td>';
            html += '<td align="left"  >&nbsp;</td><td align="center">&nbsp;</td>';
            html += '<td align="center">&nbsp;</td><td align="center">&nbsp;</td>';
            html += '<td align="right" >&nbsp;</td><td align="center">&nbsp;</td>';
        }
    }

    html += '</tr>';

    return html;
}


// ==========================================================================================================
//

function Lotto(country, name, code, fee, payout, match)
{
    this.country = country;
    this.name    = name;
    this.code    = code;
    this.fee     = fee;
    this.payout  = payout;
    this.match   = match;

    this.tickets = 0;
    this.pot     = new Rational(0,1);

    this.winner  = new LottoTotal(1);
    this.loser   = new LottoTotal(0);

    this.group   = this.match[0].group;  // just for menu icons
}

Lotto.prototype.compute = function()
{
    // Compute combinations and fair winnings

    var wins = 0;

    for(var i=0; i<this.match.length; i++)
    {
        this.match[i].compute1(this.fee, this.payout, this.match.length);

        wins += this.match[i].wins;
    }

    this.tickets = this.match[0].tickets;

    // Compute posted winnings

    this.pot.set_r(this.payout);
    this.pot.mul_r(this.fee);
    this.pot.mul_i(this.tickets);

    var temp = new Rational(0,1);
    var pool = new Rational(0,1);

    pool.set_r(this.pot);

    var norm = 0;

    for(var i=0; i<this.match.length; i++)
    {
        if(this.match[i].split)
        {
            norm += this.match[i].split;
        }
        else
        {
            temp.set_r(this.match[i].guaranteed);
            temp.mul_i(this.match[i].wins);
            pool.sub_r(temp);
        }
    }

    for(var i=0; i<this.match.length; i++)
    {
        this.match[i].compute2(this.fee, pool, norm);
    }

    delete temp;
    delete pool;

    // Compute winner and loser info

    this.winner.compute(              wins, this.tickets, this.fee, this.payout);
    this.loser.compute(this.tickets - wins, this.tickets, this.fee, this.payout);
}

Lotto.prototype.display_buttons = function()
{
    var html = '';

    html += 'The table can display <a href="error.html" onclick="update(-1, 0, -1); return false;">approximate</a>';
    html += ' or <a href="error.html" onclick="update(-1, 1, -1); return false;">exact</a> values.';

    return html;
}

Lotto.prototype.display_game = function(exact)   // ** TO DO **
{
    var html = '';
/*
    html +=     '<b>Pools:</b> One draw pool is used - balls can either be main balls or bonus balls.';
    html += '<br><b>Balls:</b> 49 uniquely numbered balls.';
    html += '<br><b>Play:</b> The player chooses 6 balls on the ticket.';
    html += '<br><b>Draw:</b> 7 balls are drawn. The first 6 balls are main balls. The final ball is the bonus ball.';
    html += '<br><b>Match:</b> There are <b>' + this.match.length + '</b> ways to win.';

    if(exact)
    {
        html += '<br><b>Fee:</b> £1 per game.';
        html += '<br><b>Payout:</b> 9 / 20 = 45.00%.';
        html += '<br><b>Tickets:</b> nCr(49, 6) = 13,983,816.';
        html += '<br><b>Pot:</b> £6,292,717.20.';
    }
    else
    {
        html += '<br><b>Fee:</b> £1 per game.';
        html += '<br><b>Payout:</b> 9 / 20 = 45.00%.';
        html += '<br><b>Tickets:</b> nCr(49, 6) = 13,983,816.';
        html += '<br><b>Pot:</b> £6,292,717.20.';
    }
*/
    return html;
}

Lotto.prototype.display_results = function(exact)
{
    var span = (exact) ? 9 : 10;

    var html = '';

    html += '<table cellspacing=1 cellpadding=8 border=0 bgcolor="#000000" class="tab">';
    html += '<tr bgcolor="#ffffff"><td colspan=' + span + '></td></tr>';
    html += '<tr bgcolor="#ffffff"><th rowspan=2><b>Match</b></th><th rowspan=2><b>Contingency</b></th><th rowspan=2><b>Tickets</b></th>';

    if(exact)
    {
        html += '<th rowspan=2><b>Probability</b></th>';
        html += '<th colspan=3><b>Posted Winnings</b></th><th colspan=2><b>Fair Winnings</b></th></tr>';
        html += '<tr bgcolor="#ffffff"><th><b>Fund</b></th><th><b>Prize</b></th><th><b>Payout</b></th><th><b>Prize</b></th><th><b>Payout</b></th></tr>';
    }
    else
    {
        html += '<th rowspan=2><b>Odds</b></th>';
        html += '<th rowspan=2 width="40"><b>Flips</b></th>';
        html += '<th colspan=3><b>Posted Winnings</b></th><th colspan=2><b>Fair Winnings</b></th></tr>';
        html += '<tr bgcolor="#ffffff"><th><b>Fund %</b></th><th><b>Prize</b></th><th><b>Payout %</b></th><th><b>Prize</b></th><th><b>Payout %</b></th></tr>';
    }

    html += '<tr bgcolor="#ffffff"><td colspan=' + span + '></td></tr>';

    for(var i=0; i<this.match.length; i++)
    {
        html += this.match[i].display(exact);
    }

    html += '<tr bgcolor="#ffffff"><td colspan=' + span + '></td></tr>';

    html += this.winner.display(exact);
    html += this.loser.display(exact);

    html += '<tr bgcolor="#ffffff"><td colspan=' + span + '></td></tr>';
    html += '</table>';

    return html;
}

Lotto.prototype.display = function(exact)
{
    var html = '<h2><img align="absbottom" class="img1" border="1" src="flags/' + this.country + '.gif"> &nbsp; ' + this.name + '</h2>';

    html += '<p>' + this.display_buttons();
    html += '<p>' + this.display_game(exact);
    html += '<p>' + this.display_results(exact);

    return html;
}


// ==========================================================================================================
//

var global_lotto_option_lotto = 0;
var global_lotto_option_exact = 0;

function update(lotto, exact, defunct)
{
    if(lotto != -1) global_lotto_option_lotto = lotto;
    if(exact != -1) global_lotto_option_exact = exact;

    var html = '<p>';

    gcd_size = gcd_succ = combs_size = combs_succ = 0;

    lottos[global_lotto_option_lotto].compute();

    html += lottos[global_lotto_option_lotto].display(global_lotto_option_exact);

//  html += '<p>' + report(gcd_size, gcd_succ) + ' &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ' + report(combs_size, combs_succ) + '<br>';

    document.getElementById('lotto').innerHTML = html;
}


// ==========================================================================================================
//

function fraction2(d)
{
    return '<table cellspacing=0 cellpadding=0 border=0 class="frac"><tr><th align="center">' + d[0] +
           '</th></tr><tr><td align="center">' + d[1] + '</td></tr></table>';
}

function split2(d)
{
    return (d[0] == 0) ? 'Fixed' : fraction2(d);
}


// ==========================================================================================================

function format(n)
{
    n += '';
    var x = n.split('.');
    x1 = x[0];
    x2 = (x.length > 1) ? '.' + x[1] : '';
    var rgx = /(\d+)(\d{3})/;
    while(rgx.test(x1)) x1 = x1.replace(rgx, '$1' + ',' + '$2');
    return x1 + x2;
}

// ==========================================================================================================

function frac(rational)
{
    return '<table cellspacing=0 cellpadding=0 border=0 class="frac"><tr><th align="center">' + format(rational.n) +
           '</th></tr><tr><td align="center">' + format(rational.d) + '</td></tr></table>';
}

// ==========================================================================================================

function odds(rational)
{
    var x = Math.round(parseFloat(rational.d) / parseFloat(rational.n));

    return('1 in ' + format(x));
}

// ==========================================================================================================

function flips(rational)
{
    var f = parseFloat(rational.d) / parseFloat(rational.n);
    var x = Math.round(Math.log(f) / Math.log(2.0));

    return(format(x));
}

// ==========================================================================================================

function money(rational)
{
    var x = parseFloat(rational.n) / parseFloat(rational.d);
    var x = x.toFixed(2);

    return(format(x));
}

// ==========================================================================================================

function twodp(rational)
{
    var r = new Rational(100, 1);

    r.mul_r(rational);

    var x = parseFloat(r.n) / parseFloat(r.d);
    var x = x.toFixed(2);

    delete r;

    return(format(x));
}

// ==========================================================================================================

function matrix(m)
{
    var t;
    var r;
    var c;

    document.writeln('<table cellspacing=1 cellpadding=0 border=0 bgcolor="#000066" class="par1"><tr>');

    for(t=0; t<m.length; t++)
    {
        document.writeln('<td bgcolor="#FFFFFF"><table cellspacing=0 cellpadding=0 border=0 bgcolor="#FFFFFF" class="par2">');

        for(r=0; r<m[t].length; r++)
        {
            document.writeln('<tr>');

            for(c=0; c<m[t][r].length; c++)
            {
                document.writeln('<td align="right">'+m[t][r][c]+'</td>');
            }

            document.writeln('</tr>');
        }

        document.writeln('</table></td>');
    }

  document.writeln('</tr></table>');
}

