/**
xxHash64 implementation in pure Javascript

Copyright (C) 2016, Pierre Curto
MIT license
*/
var UINT64 = require('cuint').UINT64

/*
 * Constants
 */
var PRIME64_1 = UINT64( '11400714785074694791' )
var PRIME64_2 = UINT64( '14029467366897019727' )
var PRIME64_3 = UINT64(  '1609587929392839161' )
var PRIME64_4 = UINT64(  '9650029242287828579' )
var PRIME64_5 = UINT64(  '2870177450012600261' )

/**
* Convert string to proper UTF-8 array
* @param str Input string
* @returns {Uint8Array} UTF8 array is returned as uint8 array
*/
function toUTF8Array (str) {
	var utf8 = []
	for (var i=0, n=str.length; i < n; i++) {
		var charcode = str.charCodeAt(i)
		if (charcode < 0x80) utf8.push(charcode)
		else if (charcode < 0x800) {
			utf8.push(0xc0 | (charcode >> 6),
			0x80 | (charcode & 0x3f))
		}
		else if (charcode < 0xd800 || charcode >= 0xe000) {
			utf8.push(0xe0 | (charcode >> 12),
			0x80 | ((charcode>>6) & 0x3f),
			0x80 | (charcode & 0x3f))
		}
		// surrogate pair
		else {
			i++;
			// UTF-16 encodes 0x10000-0x10FFFF by
			// subtracting 0x10000 and splitting the
			// 20 bits of 0x0-0xFFFFF into two halves
			charcode = 0x10000 + (((charcode & 0x3ff)<<10)
			| (str.charCodeAt(i) & 0x3ff))
			utf8.push(0xf0 | (charcode >>18),
			0x80 | ((charcode>>12) & 0x3f),
			0x80 | ((charcode>>6) & 0x3f),
			0x80 | (charcode & 0x3f))
		}
	}

	return new Uint8Array(utf8)
}

/**
 * XXH64 object used as a constructor or a function
 * @constructor
 * or
 * @param {Object|String} input data
 * @param {Number|UINT64} seed
 * @return ThisExpression
 * or
 * @return {UINT64} xxHash
 */
function XXH64 () {
	if (arguments.length == 2)
		return new XXH64( arguments[1] ).update( arguments[0] ).digest()

	if (!(this instanceof XXH64))
		return new XXH64( arguments[0] )

	init.call(this, arguments[0])
}

/**
 * Initialize the XXH64 instance with the given seed
 * @method init
 * @param {Number|Object} seed as a number or an unsigned 32 bits integer
 * @return ThisExpression
 */
 function init (seed) {
	this.seed = seed instanceof UINT64 ? seed.clone() : UINT64(seed)
	this.v1 = this.seed.clone().add(PRIME64_1).add(PRIME64_2)
	this.v2 = this.seed.clone().add(PRIME64_2)
	this.v3 = this.seed.clone()
	this.v4 = this.seed.clone().subtract(PRIME64_1)
	this.total_len = 0
	this.memsize = 0
	this.memory = null

	return this
}
XXH64.prototype.init = init

/**
 * Add data to be computed for the XXH64 hash
 * @method update
 * @param {String|Buffer|ArrayBuffer} input as a string or nodejs Buffer or ArrayBuffer
 * @return ThisExpression
 */
XXH64.prototype.update = function (input) {
	var isString = typeof input == 'string'
	var isArrayBuffer

	// Convert all strings to utf-8 first (issue #5)
	if (isString) {
		input = toUTF8Array(input)
		isString = false
		isArrayBuffer = true
	}

	if (typeof ArrayBuffer !== "undefined" && input instanceof ArrayBuffer)
	{
		isArrayBuffer = true
		input = new Uint8Array(input);
	}

	var p = 0
	var len = input.length
	var bEnd = p + len

	if (len == 0) return this

	this.total_len += len

	if (this.memsize == 0)
	{
		if (isString) {
			this.memory = ''
		} else if (isArrayBuffer) {
			this.memory = new Uint8Array(32)
		} else {
			this.memory = new Buffer(32)
		}
	}

	if (this.memsize + len < 32)   // fill in tmp buffer
	{
		// XXH64_memcpy(this.memory + this.memsize, input, len)
		if (isString) {
			this.memory += input
		} else if (isArrayBuffer) {
			this.memory.set( input.subarray(0, len), this.memsize )
		} else {
			input.copy( this.memory, this.memsize, 0, len )
		}

		this.memsize += len
		return this
	}

	if (this.memsize > 0)   // some data left from previous update
	{
		// XXH64_memcpy(this.memory + this.memsize, input, 16-this.memsize);
		if (isString) {
			this.memory += input.slice(0, 32 - this.memsize)
		} else if (isArrayBuffer) {
			this.memory.set( input.subarray(0, 32 - this.memsize), this.memsize )
		} else {
			input.copy( this.memory, this.memsize, 0, 32 - this.memsize )
		}

		var p64 = 0
		if (isString) {
			var other
			other = UINT64(
					(this.memory.charCodeAt(p64+1) << 8) | this.memory.charCodeAt(p64)
				,	(this.memory.charCodeAt(p64+3) << 8) | this.memory.charCodeAt(p64+2)
				,	(this.memory.charCodeAt(p64+5) << 8) | this.memory.charCodeAt(p64+4)
				,	(this.memory.charCodeAt(p64+7) << 8) | this.memory.charCodeAt(p64+6)
				)
			this.v1.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
			p64 += 8
			other = UINT64(
					(this.memory.charCodeAt(p64+1) << 8) | this.memory.charCodeAt(p64)
				,	(this.memory.charCodeAt(p64+3) << 8) | this.memory.charCodeAt(p64+2)
				,	(this.memory.charCodeAt(p64+5) << 8) | this.memory.charCodeAt(p64+4)
				,	(this.memory.charCodeAt(p64+7) << 8) | this.memory.charCodeAt(p64+6)
				)
			this.v2.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
			p64 += 8
			other = UINT64(
					(this.memory.charCodeAt(p64+1) << 8) | this.memory.charCodeAt(p64)
				,	(this.memory.charCodeAt(p64+3) << 8) | this.memory.charCodeAt(p64+2)
				,	(this.memory.charCodeAt(p64+5) << 8) | this.memory.charCodeAt(p64+4)
				,	(this.memory.charCodeAt(p64+7) << 8) | this.memory.charCodeAt(p64+6)
				)
			this.v3.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
			p64 += 8
			other = UINT64(
					(this.memory.charCodeAt(p64+1) << 8) | this.memory.charCodeAt(p64)
				,	(this.memory.charCodeAt(p64+3) << 8) | this.memory.charCodeAt(p64+2)
				,	(this.memory.charCodeAt(p64+5) << 8) | this.memory.charCodeAt(p64+4)
				,	(this.memory.charCodeAt(p64+7) << 8) | this.memory.charCodeAt(p64+6)
				)
			this.v4.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
		} else {
			var other
			other = UINT64(
					(this.memory[p64+1] << 8) | this.memory[p64]
				,	(this.memory[p64+3] << 8) | this.memory[p64+2]
				,	(this.memory[p64+5] << 8) | this.memory[p64+4]
				,	(this.memory[p64+7] << 8) | this.memory[p64+6]
				)
			this.v1.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
			p64 += 8
			other = UINT64(
					(this.memory[p64+1] << 8) | this.memory[p64]
				,	(this.memory[p64+3] << 8) | this.memory[p64+2]
				,	(this.memory[p64+5] << 8) | this.memory[p64+4]
				,	(this.memory[p64+7] << 8) | this.memory[p64+6]
				)
			this.v2.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
			p64 += 8
			other = UINT64(
					(this.memory[p64+1] << 8) | this.memory[p64]
				,	(this.memory[p64+3] << 8) | this.memory[p64+2]
				,	(this.memory[p64+5] << 8) | this.memory[p64+4]
				,	(this.memory[p64+7] << 8) | this.memory[p64+6]
				)
			this.v3.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
			p64 += 8
			other = UINT64(
					(this.memory[p64+1] << 8) | this.memory[p64]
				,	(this.memory[p64+3] << 8) | this.memory[p64+2]
				,	(this.memory[p64+5] << 8) | this.memory[p64+4]
				,	(this.memory[p64+7] << 8) | this.memory[p64+6]
				)
			this.v4.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
		}

		p += 32 - this.memsize
		this.memsize = 0
		if (isString) this.memory = ''
	}

	if (p <= bEnd - 32)
	{
		var limit = bEnd - 32

		do
		{
			if (isString) {
				var other
				other = UINT64(
						(input.charCodeAt(p+1) << 8) | input.charCodeAt(p)
					,	(input.charCodeAt(p+3) << 8) | input.charCodeAt(p+2)
					,	(input.charCodeAt(p+5) << 8) | input.charCodeAt(p+4)
					,	(input.charCodeAt(p+7) << 8) | input.charCodeAt(p+6)
					)
				this.v1.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
				p += 8
				other = UINT64(
						(input.charCodeAt(p+1) << 8) | input.charCodeAt(p)
					,	(input.charCodeAt(p+3) << 8) | input.charCodeAt(p+2)
					,	(input.charCodeAt(p+5) << 8) | input.charCodeAt(p+4)
					,	(input.charCodeAt(p+7) << 8) | input.charCodeAt(p+6)
					)
				this.v2.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
				p += 8
				other = UINT64(
						(input.charCodeAt(p+1) << 8) | input.charCodeAt(p)
					,	(input.charCodeAt(p+3) << 8) | input.charCodeAt(p+2)
					,	(input.charCodeAt(p+5) << 8) | input.charCodeAt(p+4)
					,	(input.charCodeAt(p+7) << 8) | input.charCodeAt(p+6)
					)
				this.v3.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
				p += 8
				other = UINT64(
						(input.charCodeAt(p+1) << 8) | input.charCodeAt(p)
					,	(input.charCodeAt(p+3) << 8) | input.charCodeAt(p+2)
					,	(input.charCodeAt(p+5) << 8) | input.charCodeAt(p+4)
					,	(input.charCodeAt(p+7) << 8) | input.charCodeAt(p+6)
					)
				this.v4.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
			} else {
				var other
				other = UINT64(
						(input[p+1] << 8) | input[p]
					,	(input[p+3] << 8) | input[p+2]
					,	(input[p+5] << 8) | input[p+4]
					,	(input[p+7] << 8) | input[p+6]
					)
				this.v1.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
				p += 8
				other = UINT64(
						(input[p+1] << 8) | input[p]
					,	(input[p+3] << 8) | input[p+2]
					,	(input[p+5] << 8) | input[p+4]
					,	(input[p+7] << 8) | input[p+6]
					)
				this.v2.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
				p += 8
				other = UINT64(
						(input[p+1] << 8) | input[p]
					,	(input[p+3] << 8) | input[p+2]
					,	(input[p+5] << 8) | input[p+4]
					,	(input[p+7] << 8) | input[p+6]
					)
				this.v3.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
				p += 8
				other = UINT64(
						(input[p+1] << 8) | input[p]
					,	(input[p+3] << 8) | input[p+2]
					,	(input[p+5] << 8) | input[p+4]
					,	(input[p+7] << 8) | input[p+6]
					)
				this.v4.add( other.multiply(PRIME64_2) ).rotl(31).multiply(PRIME64_1);
			}
			p += 8
		} while (p <= limit)
	}

	if (p < bEnd)
	{
		// XXH64_memcpy(this.memory, p, bEnd-p);
		if (isString) {
			this.memory += input.slice(p)
		} else if (isArrayBuffer) {
			this.memory.set( input.subarray(p, bEnd), this.memsize )
		} else {
			input.copy( this.memory, this.memsize, p, bEnd )
		}

		this.memsize = bEnd - p
	}

	return this
}

/**
 * Finalize the XXH64 computation. The XXH64 instance is ready for reuse for the given seed
 * @method digest
 * @return {UINT64} xxHash
 */
XXH64.prototype.digest = function () {
	var input = this.memory
	var isString = typeof input == 'string'
	var p = 0
	var bEnd = this.memsize
	var h64, h
	var u = new UINT64

	if (this.total_len >= 32)
	{
		h64 = this.v1.clone().rotl(1)
		h64.add( this.v2.clone().rotl(7) )
		h64.add( this.v3.clone().rotl(12) )
		h64.add( this.v4.clone().rotl(18) )

		h64.xor( this.v1.multiply(PRIME64_2).rotl(31).multiply(PRIME64_1) )
		h64.multiply(PRIME64_1).add(PRIME64_4)

		h64.xor( this.v2.multiply(PRIME64_2).rotl(31).multiply(PRIME64_1) )
		h64.multiply(PRIME64_1).add(PRIME64_4)

		h64.xor( this.v3.multiply(PRIME64_2).rotl(31).multiply(PRIME64_1) )
		h64.multiply(PRIME64_1).add(PRIME64_4)

		h64.xor( this.v4.multiply(PRIME64_2).rotl(31).multiply(PRIME64_1) )
		h64.multiply(PRIME64_1).add(PRIME64_4)
	}
	else
	{
		h64  = this.seed.clone().add( PRIME64_5 )
	}

	h64.add( u.fromNumber(this.total_len) )

	while (p <= bEnd - 8)
	{
		if (isString) {
			u.fromBits(
				(input.charCodeAt(p+1) << 8) | input.charCodeAt(p)
			,	(input.charCodeAt(p+3) << 8) | input.charCodeAt(p+2)
			,	(input.charCodeAt(p+5) << 8) | input.charCodeAt(p+4)
			,	(input.charCodeAt(p+7) << 8) | input.charCodeAt(p+6)
			)
		} else {
			u.fromBits(
				(input[p+1] << 8) | input[p]
			,	(input[p+3] << 8) | input[p+2]
			,	(input[p+5] << 8) | input[p+4]
			,	(input[p+7] << 8) | input[p+6]
			)
		}
		u.multiply(PRIME64_2).rotl(31).multiply(PRIME64_1)
		h64
			.xor(u)
			.rotl(27)
			.multiply( PRIME64_1 )
			.add( PRIME64_4 )
		p += 8
	}

	if (p + 4 <= bEnd) {
		if (isString) {
			u.fromBits(
				(input.charCodeAt(p+1) << 8) | input.charCodeAt(p)
			,	(input.charCodeAt(p+3) << 8) | input.charCodeAt(p+2)
			,	0
			,	0
			)
		} else {
			u.fromBits(
				(input[p+1] << 8) | input[p]
			,	(input[p+3] << 8) | input[p+2]
			,	0
			,	0
			)
		}
		h64
			.xor( u.multiply(PRIME64_1) )
			.rotl(23)
			.multiply( PRIME64_2 )
			.add( PRIME64_3 )
		p += 4
	}

	while (p < bEnd)
	{
		u.fromBits( isString ? input.charCodeAt(p++) : input[p++], 0, 0, 0 )
		h64
			.xor( u.multiply(PRIME64_5) )
			.rotl(11)
			.multiply(PRIME64_1)
	}

	h = h64.clone().shiftRight(33)
	h64.xor(h).multiply(PRIME64_2)

	h = h64.clone().shiftRight(29)
	h64.xor(h).multiply(PRIME64_3)

	h = h64.clone().shiftRight(32)
	h64.xor(h)

	// Reset the state
	this.init( this.seed )

	return h64
}

module.exports = XXH64
