/*
 * hashcash.js - hashcash anti-spam / denial of service counter-measure tool
 *
 *   Copyright (c) 2008  Takanori Ishikawa  <takanori.ishikawa@gmail.com>
 *   All rights reserved.
 *
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions
 *   are met:
 * 
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 *   3. Neither the name of the authors nor the names of its contributors
 *      may be used to endorse or promote products derived from this
 *      software without specific prior written permission.
 *
 *   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 *   OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 *   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
/**
 * This is the javascript file for code which implements the Hashcache 
 * anti-spam / denial of service counter-measure tool
 *
 *   Author: Takanori Ishikawa <takanori.ishikawa@gmail.com>
 *   Copyright: Takanori Ishikawa 2008
 *   License: BSD License (see above)
 *
 * Usage:
 *
 *   var hashcash = new Hashcash(resource, 10);
 *   hashcash.compute({
 *     async: true,
 *     onProgress: function(cyclesUsed, timeUsed) {
 *       // ...
 *     },
 *     onSuccess: function(stamp, cyclesUsed, timeUsed) {
 *       // ...
 *     }
 *   });
 *
 * Dependencies:
 *   spec.js, sha1.js
 *
 * See Also:
 *   Hashcash.org
 *   http://www.hashcash.org/
 *
 */


var Hashcash = function(resource, bits) {
  this.version = 1;
  this.date = new Date();
  this.bits = (bits || 10);
  this.resource = resource;
  this.tries = this.counter = 0;
};

Hashcash.ZERO32 = new Array(32 + 1).join("0");
Hashcash.CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";
Hashcash.MAX_INTEGER = 0x1fffffffffffff /* 53bit */
Hashcash.MAX_CYCLES = Hashcash.MAX_INTEGER;

Hashcash.CYCLES = 100;
Hashcash.DELAY = 0.01;


Hashcash.prototype = (function(){

  // Date --> "080512"
  function getTimestamp(date) {
    var items = [date.getYear(), date.getMonth() + 1, date.getDate()];
    
    if (items[0] > 100) items[0] -= 100;
    for (var i = 0; i < items.length; i++) {
      items[i] = Number(items[i]).toString(10);
      if (items[i].length == 1) items[i] = "0" + items[i];
    }
    return items.join("");
  }

  // Returns string of random characters from alphabet a-zA-Z0-9+/= (16 bytes)
  function getRandomString(length) {
    var characters = new Array(length || 16);
    
    for (var i = 0; i < characters.length; i++) {
      var idx = Math.round(Math.random() * (Hashcash.CHARACTERS.length - 1));
      characters[i] = Hashcash.CHARACTERS.charAt(idx);
    }
    return characters.join("");
  }
  
  Spec.describe("Hashcash private utilities", {
    "getTimestamp": function() {
      Spec.should.equal(getTimestamp(new Date(1999, 11, 31)), "991231");
    },
    "getRandomString": function() {
      Spec.should.not.equal(getRandomString(), getRandomString());
      Spec.should.equal(getRandomString().length, 16);
      Spec.should.equal(getRandomString(32).length, 32);
    }
  });


  return {
    onSuccess: function(stamp) {
      var timeUsed = new Date() - this.startTime;
      var cycles = this.counter;
      if (this.tries > 0) cycles += (Hashcash.MAX_CYCLES * (this.tries - 1));
      
      if (this.options.onProgress) this.options.onProgress(cycles, timeUsed);
      if (this.options.onSuccess) this.options.onSuccess(stamp, cycles, timeUsed);
      
      this.startTime = null;
    },

    mint: function() {
      return [
        this.version,
        this.bits,
        getTimestamp(this.date),
        encodeURIComponent(this.resource),
        "", // extension -- ignored in the current version 
        getRandomString(), // rand
      ].join(":");
    },

    cycle: function() {
      if (this._mint == null || this.counter >= Hashcash.MAX_CYCLES) {
        this._mint = this.mint();
        this.counter = 0;
        this.tries++; // challenge new mint
      }
      
      var ctr = Number(this.counter++).toString(16);
      if (ctr.length < 18) ctr = Hashcash.ZERO32.substr(0, 18 - ctr.length) + ctr
      
      var stamp = [this._mint, ":", ctr].join("");
      var digest = new SHA1(stamp).digest();
      var nbits = (32 - Number(digest[0]).toString(2).length);
      return (nbits >= this.bits) ? stamp : null;
    },

    _async: function() {
      if (this.options.onProgress != null) {
        var timeUsed = new Date() - this.startTime;
        var cycles = this.counter;
        if (this.tries > 1) cycles += (Hashcash.MAX_CYCLES * (this.tries - 1));
        this.options.onProgress(cycles, timeUsed);
      }
      for (var i = 0; i < Hashcash.CYCLES; i++) {
        var stamp = this.cycle();
        if (stamp != null) return this.onSuccess(stamp);
      }
      var method = arguments.callee, object = this;
      setTimeout(function(){ method.apply(object); }, Hashcash.DELAY);
    },

    bench: function() {
      var times = 5000;
      var startTime = new Date();
      for (var i = 0; i < times; i++) this.cycle();
      alert(((new Date() - startTime)/times) + "ms");
    },

    compute: function(options) {
      this.options = options;
      this.startTime = new Date();
      this._mint = null;
      this.tries = this.counter = 0;
      
      if (this.options.async) {
        var object = this;
        setTimeout(function(){ object._async(); }, 1);
        return;
      }
      
      while (true) {
        var stamp = this.cycle();
        if (stamp != null) return stamp;
      }
    },

    toString: function() {
      return "Hashcash <" + this.resource + "> " + this.bits + "bits";
    }
  };
})();

