// Copyright (C) 2006 and onwards Google Inc.

/**
 * Implementation of HMAC in JavaScript.
 *
 * @constructor
 * @param hasher    An object implementing reset(), update(), and digest() to
 *                  serve as a hash function. See google3/javascript/sha1.js for
 *                  example.
 *
 * @param key       The secret key to use to calculate the hmac. Should be an
 *                  array of not more than @blockSize integers in {0, 255}.
 *
 * @param opt_blockSize Optional. The block size @hasher uses. If not specified, 16.
 */
function G_HMAC(hasher, key, opt_blockSize) {
  if (!hasher || typeof hasher != "object" || !hasher.reset ||
      !hasher.update || !hasher.digest) {
    throw new Error("Invalid hasher object. Hasher unspecified or does not " +
                    "implement expected interface.");
  }

  if (key.constructor != Array) {
    throw new Error("Invalid key.");
  }

  if (opt_blockSize && typeof opt_blockSize != "number") {
    throw new Error("Invalid block size.");
  }

  this.hasher_ = hasher;
  this.blockSize_ = opt_blockSize || 16;
  this.keyO_ = new Array(this.blockSize_);
  this.keyI_ = new Array(this.blockSize_);

  // If key is too long, first hash it per spec.
  if (key.length > this.blockSize_) {
    this.hasher_.update(key);
    key = this.hasher_.digest();
  }

  // precalculate padded and xor'd keys.
  var keyByte;
  for (var i = 0; i < this.blockSize_; i++) {
    if (i < key.length) {
      keyByte = key[i];
    } else {
      keyByte = 0; 
    }
    
    this.keyO_[i] = keyByte ^ G_HMAC.OPAD;
    this.keyI_[i] = keyByte ^ G_HMAC.IPAD;
  }
}

G_HMAC.OPAD = 0x5c;
G_HMAC.IPAD = 0x36;

/**
 * Resets the HMAC to the state it was in after construction.
 */
G_HMAC.prototype.reset = function() {
  this.hasher_.reset();
  this.hasher_.update(this.keyI_);
};

/**
 * Updates the hash with additional data.
 */
G_HMAC.prototype.update = function(data) {
  if (data.constructor != Array) {
    throw new Error("Invalid data. Data must be an array.");
  }

  this.hasher_.update(data);
};

/**
 * Completes hashing and returns a digest as a byte array.
 */
G_HMAC.prototype.digest = function() {
  var temp = this.hasher_.digest();

  this.hasher_.reset();
  this.hasher_.update(this.keyO_);
  this.hasher_.update(temp);

  return this.hasher_.digest();
};

/**
 * Calculates an HMAC for a given message.
 *
 * @param message  An array of integers in {0, 255}.
 */
G_HMAC.prototype.getHMac = function(message) {
  this.reset();
  this.update(message);

  return this.digest();
}


// (c) Copyright Google Inc. 2005

/**
 * SHA-1 cryptographic hash
 * @constructor
 */
function SHA1() {
  this.chain_ = new Array(5);
  this.buf_ = new Array(64);
  this.W_ = new Array(80);
  this.pad_ = new Array(64);
  this.pad_[0] = 128;
  for (var i = 1; i < 64; ++i) {
    this.pad_[i] = 0;
  }

  this.reset();
}

/**
 * Reset the internal accumulator
 */
SHA1.prototype.reset = function() {
  this.chain_[0] = 0x67452301;
  this.chain_[1] = 0xefcdab89;
  this.chain_[2] = 0x98badcfe;
  this.chain_[3] = 0x10325476;
  this.chain_[4] = 0xc3d2e1f0;

  this.inbuf_ = 0;
  this.total_ = 0;
}

/**
 * Internal helper performing 32 bit left rotate
 * @returns {number} w rotated left by r bits
 * @private
 */
SHA1.prototype.rotl_ = function(w, r) {
  return ((w << r) | (w >>> (32 - r))) & 0xffffffff;
}

/**
 * Internal compress helper function
 * @param {Array} buf containing block to compress
 * @private
 */
SHA1.prototype.compress_ = function(buf) {
  var W = this.W_;

  // get 16 big endian words
  for (var i = 0; i < 64; i += 4) {
    var w = (buf[i] << 24) |
            (buf[i+1] << 16) |
            (buf[i+2] << 8) |
            (buf[i+3]);
    W[i / 4] = w;
  }

  // expand to 80 words
  for (var i = 16; i < 80; ++i) {
    W[i] = this.rotl_(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1);
  }

  var A = this.chain_[0];
  var B = this.chain_[1];
  var C = this.chain_[2];
  var D = this.chain_[3];
  var E = this.chain_[4];
  var f, k;

  for (var i = 0; i < 80; ++i) {
    if (i < 40) {
      if (i < 20) {
        f = D ^ (B & (C ^ D));
        k = 0x5a827999;
      } else {
        f = B ^ C ^ D;
        k = 0x6ed9eba1;
      }
    } else {
      if (i < 60) {
        f = (B & C) | (D & (B | C));
        k = 0x8f1bbcdc;
      } else {
        f = B ^ C ^ D;
        k = 0xca62c1d6;
      }
    }

    var t = (this.rotl_(A, 5) + f + E + k + W[i]) & 0xffffffff;
    E = D;
    D = C;
    C = this.rotl_(B, 30);
    B = A;
    A = t;
  }

  this.chain_[0] = (this.chain_[0] + A) & 0xffffffff;
  this.chain_[1] = (this.chain_[1] + B) & 0xffffffff;
  this.chain_[2] = (this.chain_[2] + C) & 0xffffffff;
  this.chain_[3] = (this.chain_[3] + D) & 0xffffffff;
  this.chain_[4] = (this.chain_[4] + E) & 0xffffffff;
}

/**
 * Add byte array to internal accumulator
 * @param {Array} bytes to add to digest
 * @param {number} opt_length is # of bytes to compress
 */
SHA1.prototype.update = function(bytes, opt_length) {
  if (!opt_length) {
    opt_length = bytes.length;
  }

  var n = 0;

  // optimize for 64 byte chunks at 64 byte boundaries..
  if (this.inbuf_ == 0) {
    while (n + 64 < opt_length) {
      this.compress_(bytes.slice(n, n + 64));
      n += 64;
      this.total_ += 64;
    }
  }

  while (n < opt_length) {
    this.buf_[this.inbuf_++] = bytes[n++];
    ++this.total_;

    if (this.inbuf_ == 64) {
      this.inbuf_ = 0;
      this.compress_(this.buf_);

      // pick up 64 byte chunks..
      while (n + 64 < opt_length) {
        this.compress_(bytes.slice(n, n + 64));
        n += 64;
        this.total_ += 64;
      }
    }
  }
}

/**
 * @returns {Array} byte[20] containing finalized hash
 */
SHA1.prototype.digest = function() {
  var digest = new Array(20);
  var totalBits = this.total_ * 8;

  // add pad 0x80 0x00*
  if (this.inbuf_ < 56) {
    this.update(this.pad_, 56 - this.inbuf_);
  } else {
    this.update(this.pad_, 64 - (this.inbuf_ - 56));
  }

  // add # bits
  for (var i = 63; i >= 56; --i) {
    this.buf_[i] = totalBits & 255;
    totalBits >>>= 8;
    }

  this.compress_(this.buf_);

  var n = 0;
  for (var i = 0; i < 5; ++i) {
    for (var j = 24; j >= 0; j -= 8) {
      digest[n++] = (this.chain_[i] >> j) & 255;
    }
  }

  return digest;
}


// For jobs or internships at GameChanger, please e-mail us at:
// 
//     afedorov@gamechanger.io
//
// For a personal 'in', try anfedorov@gmail.com ... and mention coleslaw in the subject line ;)

function khmac(/*key, ...rest*/) {
    function str2arr(s) {
        var arr = [];
        for (var i = 0; i < s.length; i++) {
            arr[i] = s.charCodeAt(i);
        }
        return arr;
    }

    var key = arguments[0],
        msgs = Array.prototype.slice.call(arguments, 1),
        ret = "";
    
    var h = new G_HMAC(new SHA1(), str2arr(key), 64);
    h.reset();
    
    for (var i = 0; i < msgs.length; i++) {
        h.update(str2arr(msgs[i]));
    }
    
    var digest = h.digest();
    
    for (var i = 0; i < digest.length; i++) {
        var digit = digest[i].toString(16);
        ret += digit.length == 1 ? "0" + digit : digit;
    }
    
    return ret;
}

function call(api, data, callback, type, json) {
    var path = '/api/'+api;
    type = type || "GET";
    jQuery.ajax({
        'url': path,
        'data': { 'json': JSON.stringify(data) },
        'type': type,
        'beforeSend': function (xhr) {
            var h = khmac(GC.session_token, path + "hithere")
            xhr.setRequestHeader("GC_AUTH_API_CLIENT", "webfront");
            xhr.setRequestHeader("GC_AUTH_EMAIL", GC.email);
            xhr.setRequestHeader("GC_AUTH_HASH", h);
            xhr.setRequestHeader("GC_AUTH_TIMESTAMP", "hithere");
        },
        'success': function (data) {
            if (GC.debug) {
                console.log(JSON.stringify(JSON.parse(data), undefined, 4))
            }
            if (callback) {
                callback(json ? JSON.parse(data) : data);
            }
        }
    })
}