var mongodb = require('../../lib/bson').pure();

var testCase = require('nodeunit').testCase,
  mongoO = require('../../lib/bson').pure(),
  debug = require('util').debug,
  inspect = require('util').inspect,
  Buffer = require('buffer').Buffer,
  gleak = require('../../tools/gleak'),
  fs = require('fs'),
  BSON = mongoO.BSON,
  Code = mongoO.Code, 
  Binary = mongoO.Binary,
  Timestamp = mongoO.Timestamp,
  Long = mongoO.Long,
  MongoReply = mongoO.MongoReply,
  ObjectID = mongoO.ObjectID,
  Symbol = mongoO.Symbol,
  DBRef = mongoO.DBRef,
  Double = mongoO.Double,
  MinKey = mongoO.MinKey,
  MaxKey = mongoO.MaxKey,
  BinaryParser = mongoO.BinaryParser,
  utils = require('./tools/utils');

var BSONSE = mongodb,
  BSONDE = mongodb;

// for tests
BSONDE.BSON_BINARY_SUBTYPE_DEFAULT = 0;
BSONDE.BSON_BINARY_SUBTYPE_FUNCTION = 1;
BSONDE.BSON_BINARY_SUBTYPE_BYTE_ARRAY = 2;
BSONDE.BSON_BINARY_SUBTYPE_UUID = 3;
BSONDE.BSON_BINARY_SUBTYPE_MD5 = 4;
BSONDE.BSON_BINARY_SUBTYPE_USER_DEFINED = 128;          

BSONSE.BSON_BINARY_SUBTYPE_DEFAULT = 0;
BSONSE.BSON_BINARY_SUBTYPE_FUNCTION = 1;
BSONSE.BSON_BINARY_SUBTYPE_BYTE_ARRAY = 2;
BSONSE.BSON_BINARY_SUBTYPE_UUID = 3;
BSONSE.BSON_BINARY_SUBTYPE_MD5 = 4;
BSONSE.BSON_BINARY_SUBTYPE_USER_DEFINED = 128;          

var hexStringToBinary = function(string) {
  var numberofValues = string.length / 2;
  var array = "";
  
  for(var i = 0; i < numberofValues; i++) {
    array += String.fromCharCode(parseInt(string[i*2] + string[i*2 + 1], 16));
  }  
  return array;
}

var assertBuffersEqual = function(test, buffer1, buffer2) {  
  if(buffer1.length != buffer2.length) test.fail("Buffers do not have the same length", buffer1, buffer2);
  
  for(var i = 0; i < buffer1.length; i++) {
    test.equal(buffer1[i], buffer2[i]);
  }
}

/**
 * Module for parsing an ISO 8601 formatted string into a Date object.
 */
var ISODate = function (string) {
  var match;

 if (typeof string.getTime === "function")
   return string;
 else if (match = string.match(/^(\d{4})(-(\d{2})(-(\d{2})(T(\d{2}):(\d{2})(:(\d{2})(\.(\d+))?)?(Z|((\+|-)(\d{2}):(\d{2}))))?)?)?$/)) {
   var date = new Date();
   date.setUTCFullYear(Number(match[1]));
   date.setUTCMonth(Number(match[3]) - 1 || 0);
   date.setUTCDate(Number(match[5]) || 0);
   date.setUTCHours(Number(match[7]) || 0);
   date.setUTCMinutes(Number(match[8]) || 0);
   date.setUTCSeconds(Number(match[10]) || 0);
   date.setUTCMilliseconds(Number("." + match[12]) * 1000 || 0);

   if (match[13] && match[13] !== "Z") {
     var h = Number(match[16]) || 0,
         m = Number(match[17]) || 0;

     h *= 3600000;
     m *= 60000;

     var offset = h + m;
     if (match[15] == "+")
       offset = -offset;

     date = new Date(date.valueOf() + offset);
   }

   return date;
 } else
   throw new Error("Invalid ISO 8601 date given.", __filename);
};

/**
 * Retrieve the server information for the current
 * instance of the db client
 * 
 * @ignore
 */
exports.setUp = function(callback) {
  callback();
}

/**
 * Retrieve the server information for the current
 * instance of the db client
 * 
 * @ignore
 */
exports.tearDown = function(callback) {
  callback();
}

/**
 * @ignore
 */  
exports.shouldCorrectlyDeserializeUsingTypedArray = function(test) {
  if(typeof ArrayBuffer == 'undefined') {
    test.done();
    return;
  }
  
  var motherOfAllDocuments = {
    'string': '客家话',
    'array': [1,2,3],
    'hash': {'a':1, 'b':2},
    'date': new Date(),
    'oid': new ObjectID(),
    'binary': new Binary(new Buffer("hello")),
    'int': 42,
    'float': 33.3333,
    'regexp': /regexp/,
    'boolean': true,
    'long': Long.fromNumber(100),
    'where': new Code('this.a > i', {i:1}),        
    'dbref': new DBRef('namespace', new ObjectID(), 'integration_tests_'),
    'minkey': new MinKey(),
    'maxkey': new MaxKey()    
  }
  
  // Let's serialize it
  var data = BSONSE.BSON.serialize(motherOfAllDocuments, true, true, false);
  // Build a typed array
  var arr = new Uint8Array(new ArrayBuffer(data.length));
  // Iterate over all the fields and copy
  for(var i = 0; i < data.length; i++) {
    arr[i] = data[i]
  }
  
  // Deserialize the object
  var object = BSONDE.BSON.deserialize(arr);
  // Asserts
  test.equal(motherOfAllDocuments.string, object.string);
  test.deepEqual(motherOfAllDocuments.array, object.array);
  test.deepEqual(motherOfAllDocuments.date, object.date);
  test.deepEqual(motherOfAllDocuments.oid.toHexString(), object.oid.toHexString());
  test.deepEqual(motherOfAllDocuments.binary.length(), object.binary.length());
  // Assert the values of the binary
  for(var i = 0; i < motherOfAllDocuments.binary.length(); i++) {
    test.equal(motherOfAllDocuments.binary.value[i], object.binary[i]);
  }
  test.deepEqual(motherOfAllDocuments.int, object.int);
  test.deepEqual(motherOfAllDocuments.float, object.float);
  test.deepEqual(motherOfAllDocuments.regexp, object.regexp);
  test.deepEqual(motherOfAllDocuments.boolean, object.boolean);
  test.deepEqual(motherOfAllDocuments.long.toNumber(), object.long);
  test.deepEqual(motherOfAllDocuments.where, object.where);
  test.deepEqual(motherOfAllDocuments.dbref.oid.toHexString(), object.dbref.oid.toHexString());
  test.deepEqual(motherOfAllDocuments.dbref.namespace, object.dbref.namespace);
  test.deepEqual(motherOfAllDocuments.dbref.db, object.dbref.db);
  test.deepEqual(motherOfAllDocuments.minkey, object.minkey);
  test.deepEqual(motherOfAllDocuments.maxkey, object.maxkey);
  test.done();
}

/**
 * @ignore
 */  
exports.shouldCorrectlySerializeUsingTypedArray = function(test) {
  if(typeof ArrayBuffer == 'undefined') {
    test.done();
    return;
  }

  var motherOfAllDocuments = {
    'string': 'hello',
    'array': [1,2,3],
    'hash': {'a':1, 'b':2},
    'date': new Date(),
    'oid': new ObjectID(),
    'binary': new Binary(new Buffer("hello")),
    'int': 42,
    'float': 33.3333,
    'regexp': /regexp/,
    'boolean': true,
    'long': Long.fromNumber(100),
    'where': new Code('this.a > i', {i:1}),        
    'dbref': new DBRef('namespace', new ObjectID(), 'integration_tests_'),
    'minkey': new MinKey(),
    'maxkey': new MaxKey()    
  }
  
  // Let's serialize it
  var data = BSONSE.BSON.serialize(motherOfAllDocuments, true, false, false);
  // And deserialize it again
  var object = BSONSE.BSON.deserialize(data);
  // Asserts
  test.equal(motherOfAllDocuments.string, object.string);
  test.deepEqual(motherOfAllDocuments.array, object.array);
  test.deepEqual(motherOfAllDocuments.date, object.date);
  test.deepEqual(motherOfAllDocuments.oid.toHexString(), object.oid.toHexString());
  test.deepEqual(motherOfAllDocuments.binary.length(), object.binary.length());
  // Assert the values of the binary
  for(var i = 0; i < motherOfAllDocuments.binary.length(); i++) {
    test.equal(motherOfAllDocuments.binary.value[i], object.binary[i]);
  }
  test.deepEqual(motherOfAllDocuments.int, object.int);
  test.deepEqual(motherOfAllDocuments.float, object.float);
  test.deepEqual(motherOfAllDocuments.regexp, object.regexp);
  test.deepEqual(motherOfAllDocuments.boolean, object.boolean);
  test.deepEqual(motherOfAllDocuments.long.toNumber(), object.long);
  test.deepEqual(motherOfAllDocuments.where, object.where);
  test.deepEqual(motherOfAllDocuments.dbref.oid.toHexString(), object.dbref.oid.toHexString());
  test.deepEqual(motherOfAllDocuments.dbref.namespace, object.dbref.namespace);
  test.deepEqual(motherOfAllDocuments.dbref.db, object.dbref.db);
  test.deepEqual(motherOfAllDocuments.minkey, object.minkey);
  test.deepEqual(motherOfAllDocuments.maxkey, object.maxkey);
  test.done();
}

/**
 * @ignore
 */  
exports['exercise all the binary object constructor methods'] = function (test) {
  if(typeof ArrayBuffer == 'undefined') {
    test.done();
    return;
  }

  // Construct using array
  var string = 'hello world';
  // String to array
  var array = utils.stringToArrayBuffer(string);

  // Binary from array buffer
  var binary = new Binary(utils.stringToArrayBuffer(string));
  test.ok(string.length, binary.buffer.length);
  test.ok(utils.assertArrayEqual(array, binary.buffer));
  
  // Construct using number of chars
  binary = new Binary(5);
  test.ok(5, binary.buffer.length);
  
  // Construct using an Array
  var binary = new Binary(utils.stringToArray(string));
  test.ok(string.length, binary.buffer.length);
  test.ok(utils.assertArrayEqual(array, binary.buffer));
  
  // Construct using a string
  var binary = new Binary(string);
  test.ok(string.length, binary.buffer.length);
  test.ok(utils.assertArrayEqual(array, binary.buffer));
  test.done();
};

/**
 * @ignore
 */  
exports['exercise the put binary object method for an instance when using Uint8Array'] = function (test) {
  if(typeof ArrayBuffer == 'undefined') {
    test.done();
    return;
  }

  // Construct using array
  var string = 'hello world';
  // String to array
  var array = utils.stringToArrayBuffer(string + 'a');
  
  // Binary from array buffer
  var binary = new Binary(utils.stringToArrayBuffer(string));
  test.ok(string.length, binary.buffer.length);

  // Write a byte to the array
  binary.put('a')

  // Verify that the data was writtencorrectly
  test.equal(string.length + 1, binary.position);
  test.ok(utils.assertArrayEqual(array, binary.value(true)));
  test.equal('hello worlda', binary.value());

  // Exercise a binary with lots of space in the buffer
  var binary = new Binary();
  test.ok(Binary.BUFFER_SIZE, binary.buffer.length);

  // Write a byte to the array
  binary.put('a')

  // Verify that the data was writtencorrectly
  test.equal(1, binary.position);
  test.ok(utils.assertArrayEqual(['a'.charCodeAt(0)], binary.value(true)));
  test.equal('a', binary.value());
  test.done();
},

/**
 * @ignore
 */  
exports['exercise the write binary object method for an instance when using Uint8Array'] = function (test) {
  if(typeof ArrayBuffer == 'undefined') {
    test.done();
    return;
  }

  // Construct using array
  var string = 'hello world';
  // Array
  var writeArrayBuffer = new Uint8Array(new ArrayBuffer(1));
  writeArrayBuffer[0] = 'a'.charCodeAt(0);
  var arrayBuffer = ['a'.charCodeAt(0)];
  
  // Binary from array buffer
  var binary = new Binary(utils.stringToArrayBuffer(string));
  test.ok(string.length, binary.buffer.length);

  // Write a string starting at end of buffer
  binary.write('a');
  test.equal('hello worlda', binary.value());
  // Write a string starting at index 0
  binary.write('a', 0);
  test.equal('aello worlda', binary.value());
  // Write a arraybuffer starting at end of buffer
  binary.write(writeArrayBuffer);
  test.equal('aello worldaa', binary.value());
  // Write a arraybuffer starting at position 5
  binary.write(writeArrayBuffer, 5);
  test.equal('aelloaworldaa', binary.value());
  // Write a array starting at end of buffer
  binary.write(arrayBuffer);
  test.equal('aelloaworldaaa', binary.value());
  // Write a array starting at position 6
  binary.write(arrayBuffer, 6);
  test.equal('aelloaaorldaaa', binary.value());
  test.done();
},

/**
 * @ignore
 */  
exports['exercise the read binary object method for an instance when using Uint8Array'] = function (test) {      
  if(typeof ArrayBuffer == 'undefined') {
    test.done();
    return;
  }

  // Construct using array
  var string = 'hello world';
  var array = utils.stringToArrayBuffer(string);

  // Binary from array buffer
  var binary = new Binary(utils.stringToArrayBuffer(string));
  test.ok(string.length, binary.buffer.length);
  
  // Read the first 2 bytes
  var data = binary.read(0, 2);
  test.ok(utils.assertArrayEqual(utils.stringToArrayBuffer('he'), data));

  // Read the entire field
  var data = binary.read(0);
  test.ok(utils.assertArrayEqual(utils.stringToArrayBuffer(string), data));

  // Read 3 bytes
  var data = binary.read(6, 5);
  test.ok(utils.assertArrayEqual(utils.stringToArrayBuffer('world'), data));
  test.done();
}

/**
 * Retrieve the server information for the current
 * instance of the db client
 * 
 * @ignore
 */
exports.noGlobalsLeaked = function(test) {
  var leaks = gleak.detectNew();
  test.equal(0, leaks.length, "global var leak detected: " + leaks.join(', '));
  test.done();
}