//
// Utilities related to background operations while other operations are working
//

/**
 * Allows synchronization between background ops and the test operations
 */
var waitForLock = function(mongo, name) {
  var ts = new ObjectId();
  var lockColl = mongo.getCollection("config.testLocks");

  lockColl.update({_id: name, state: 0}, {$set: {state: 0}}, true);

  //
  // Wait until we can set the state to 1 with our id
  //

  var startTime = new Date().getTime();

  assert.soon(function() {
    lockColl.update({_id: name, state: 0}, {$set: {ts: ts, state: 1}});
    var gleObj = lockColl.getDB().getLastErrorObj();

    if (new Date().getTime() - startTime > 20 * 1000) {
      print("Waiting for...");
      printjson(gleObj);
      printjson(lockColl.findOne());
      printjson(ts);
    }

    return gleObj.n === 1 || gleObj.updatedExisting;
  }, "could not acquire lock", 30 * 1000, 100);

  print("Acquired lock " + tojson({_id: name, ts: ts}) + " curr : " +
           tojson(lockColl.findOne({_id: name})));

  // Set the state back to 0
  var unlock = function() {
    print("Releasing lock " + tojson({_id: name, ts: ts}) + " curr : " +
                tojson(lockColl.findOne({_id: name})));
    lockColl.update({_id: name, ts: ts}, {$set: {state: 0}});
  };

  // Return an object we can invoke unlock on
  return {unlock: unlock};
};

/**
 * Allows a test or background op to say it's finished
 */
var setFinished = function(mongo, name, finished) {
  if (finished || finished === undefined || finished === null) {
    mongo.getCollection("config.testFinished").update({_id: name}, {_id: name}, true);
  } else {
    mongo.getCollection("config.testFinished").remove({_id: name});
  }
};

/**
 * Checks whether a test or background op is finished
 */
var isFinished = function(mongo, name) {
  return mongo.getCollection("config.testFinished").findOne({_id: name}) !== null;
};

/**
 * Sets the result of a background op
 */
var setResult = function(mongo, name, result, err) {
  mongo.getCollection("config.testResult").update({_id: name}, {_id: name, result: result, err: err}, true);
};

/**
 * Gets the result for a background op
 */
var getResult = function(mongo, name) {
  return mongo.getCollection("config.testResult").findOne({_id: name});
};

/**
 * Overrides the parallel shell code in mongo
 */
function startParallelShell(jsCode, port) {
  var x;
  if (port) {
    x = startMongoProgramNoConnect("mongo", "--port", port, "--eval", jsCode);
  } else {
    x = startMongoProgramNoConnect("mongo", "--eval", jsCode, db ? db.getMongo().host : null);
  }

  return function() {
    jsTestLog("Waiting for shell " + x + "...");
    waitProgram(x);
    jsTestLog("Shell " + x + " finished.");
  };
}

var RandomFunctionContext = function(context) {
  Random.srand(context.seed);
  Random.randBool = function() {
    return Random.rand() > 0.5;
  };

  Random.randInt = function(min, max) {
    if (max === undefined) {
      max = min;
      min = 0;
    }
    return min + Math.floor(Random.rand() * max);
  };

  Random.randShardKey = function() {
    var numFields = 2; // Random.randInt(1, 3)
    var key = {};
    for (var i = 0; i < numFields; i++) {
      var field = String.fromCharCode("a".charCodeAt() + i);
      key[field] = 1;
    }
    return key;
  };

  Random.randShardKeyValue = function(shardKey) {
    var keyValue = {};
    for (field in shardKey) {
      if (!shardKey.hasOwnProperty(field)) {
        continue;
      }
      keyValue[field] = Random.randInt(1, 100);
    }
    return keyValue;
  };

  Random.randCluster = function() {
    var numShards = 2; // Random.randInt( 1, 10 )
    var rs = false; // Random.randBool()
    var st = new ShardingTest({
      shards: numShards,
      mongos: 4,
      other: {separateConfig: true, rs: rs}
    });
    return st;
  };
};


startParallelOps = function(mongo, proc, args, context) {
  var procName = proc.name + "-" + new ObjectId();
  var seed = new ObjectId(new ObjectId().valueOf().split("").reverse().join(""))
    .getTimestamp().getTime();

  // Make sure we aren't finished before we start
  setFinished(mongo, procName, false);
  setResult(mongo, procName, undefined, undefined);

  // TODO: Make this a context of its own
  var procContext = {
    procName: procName,
    seed: seed,
    waitForLock: waitForLock,
    setFinished: setFinished,
    isFinished: isFinished,
    setResult: setResult,
    setup: function(context, stored) {
      waitForLock = function() {
        return context.waitForLock(db.getMongo(), context.procName);
      };
      setFinished = function(finished) {
        return context.setFinished(db.getMongo(), context.procName, finished);
      };
      isFinished = function() {
        return context.isFinished(db.getMongo(), context.procName);
      };
      setResult = function(result, err) {
        return context.setResult(db.getMongo(), context.procName, result, err);
      };
    },
  };

  var bootstrapper = function(stored) {
    var procContext = stored.procContext;
    procContext.setup(procContext, stored);

    var contexts = stored.contexts;
    eval("contexts = " + contexts); // eslint-disable-line no-eval

    for (var i = 0; i < contexts.length; i++) {
      if (typeof (contexts[i]) !== "undefined") {
        // Evaluate all contexts
        contexts[i](procContext);
      }
    }

    var operation = stored.operation;
    eval("operation = " + operation); // eslint-disable-line no-eval

    var args = stored.args;
    eval("args = " + args); // eslint-disable-line no-eval

    result = undefined;
    err = undefined;

    try {
      result = operation.apply(null, args);
    } catch (e) {
      err = e;
    }

    setResult(result, err);
  };

  var contexts = [RandomFunctionContext, context];

  var testDataColl = mongo.getCollection("config.parallelTest");

  testDataColl.insert({
    _id: procName,
    bootstrapper: tojson(bootstrapper),
    operation: tojson(proc),
    args: tojson(args),
    procContext: procContext,
    contexts: tojson(contexts),
  });

  assert.eq(null, testDataColl.getDB().getLastError());

  var bootstrapStartup =
        "{ var procName = '" + procName + "'; " +
          "var stored = db.getMongo().getCollection( '" + testDataColl + "' )" +
                         ".findOne({ _id : procName }); " +
          "var bootstrapper = stored.bootstrapper; " +
          "eval( 'bootstrapper = ' + bootstrapper ); " +
          "bootstrapper( stored ); " +
        "}";


  var oldDB = db;
  db = mongo.getDB("test"); // eslint-disable-line no-native-reassign

  jsTest.log("Starting " + proc.name + " operations...");

  var rawJoin = startParallelShell(bootstrapStartup);

  db = oldDB; // eslint-disable-line no-native-reassign


  var join = function() {
    setFinished(mongo, procName, true);

    rawJoin();
    result = getResult(mongo, procName);

    assert.neq(result, null);

    if (result.err) {
      throw Error("Error in parallel ops " + procName + " : "
          + tojson(result.err));
    }
    return result.result;
  };

  join.isFinished = function() {
    return isFinished(mongo, procName);
  };

  join.setFinished = function(finished) {
    return setFinished(mongo, procName, finished);
  };

  join.waitForLock = function(name) {
    return waitForLock(mongo, name);
  };

  return join;
};


//
// Some utility operations
//

function moveOps(collName, options) {
  options = options || {};

  var admin = db.getMongo().getDB("admin");
  var config = db.getMongo().getDB("config");
  var shards = config.shards.find().toArray();
  var shardKey = config.collections.findOne({_id: collName}).key;

  while (!isFinished()) {
    var findKey = Random.randShardKeyValue(shardKey);
    var toShard = shards[Random.randInt(shards.length)]._id;

    try {
      printjson(admin.runCommand({
        moveChunk: collName,
        find: findKey,
        to: toShard,
      }));
    } catch (e) {
      printjson(e);
    }

    sleep(1000);
  }

  jsTest.log("Stopping moveOps...");
}

function splitOps(collName, options) {
  options = options || {};

  var admin = db.getMongo().getDB("admin");
  var config = db.getMongo().getDB("config");
  var shards = config.shards.find().toArray();
  var shardKey = config.collections.findOne({_id: collName}).key;

  while (!isFinished()) {
    var middleKey = Random.randShardKeyValue(shardKey);

    try {
      printjson(admin.runCommand({
        split: collName,
        middle: middleKey,
      }));
    } catch (e) {
      printjson(e);
    }

    sleep(1000);
  }

  jsTest.log("Stopping splitOps...");
}

