function CronJob(CronTime, spawn) {
	function fnWrap(cmd) {
		let command;
		let args;

		switch (typeof cmd) {
			case 'string':
				args = cmd.split(' ');
				command = args.shift();

				return spawn.bind(undefined, command, args);

			case 'object':
				command = cmd && cmd.command;
				if (command) {
					args = cmd.args;
					const options = cmd.options;

					return spawn.bind(undefined, command, args, options);
				}
				break;
		}

		return cmd;
	}

	function CJ(
		cronTime,
		onTick,
		onComplete,
		startNow,
		timeZone,
		context,
		runOnInit,
		utcOffset,
		unrefTimeout
	) {
		let _cronTime = cronTime;
		let argCount = 0;
		for (let i = 0; i < arguments.length; i++) {
			if (arguments[i] !== undefined) {
				argCount++;
			}
		}

		if (typeof cronTime !== 'string' && argCount === 1) {
			// crontime is an object...
			onTick = cronTime.onTick;
			onComplete = cronTime.onComplete;
			context = cronTime.context;
			startNow = cronTime.start || cronTime.startNow || cronTime.startJob;
			timeZone = cronTime.timeZone;
			runOnInit = cronTime.runOnInit;
			_cronTime = cronTime.cronTime;
			utcOffset = cronTime.utcOffset;
			unrefTimeout = cronTime.unrefTimeout;
		}

		this.context = context || this;
		this._callbacks = [];
		this.onComplete = fnWrap(onComplete);
		this.cronTime = new CronTime(_cronTime, timeZone, utcOffset);
		this.unrefTimeout = unrefTimeout;

		addCallback.call(this, fnWrap(onTick));

		if (runOnInit) {
			this.lastExecution = new Date();
			fireOnTick.call(this);
		}

		if (startNow) {
			start.call(this);
		}

		return this;
	}

	const addCallback = function (callback) {
		if (typeof callback === 'function') {
			this._callbacks.push(callback);
		}
	};
	CJ.prototype.addCallback = addCallback;

	CJ.prototype.setTime = function (time) {
		if (typeof time !== 'object') {
			// crontime is an object...
			throw new Error('time must be an instance of CronTime.');
		}
		const wasRunning = this.running;
		this.stop();
		this.cronTime = time;
		if (wasRunning) this.start();
	};

	CJ.prototype.nextDate = function () {
		return this.cronTime.sendAt();
	};

	const fireOnTick = function () {
		for (let i = this._callbacks.length - 1; i >= 0; i--) {
			this._callbacks[i].call(this.context, this.onComplete);
		}
	};
	CJ.prototype.fireOnTick = fireOnTick;

	CJ.prototype.nextDates = function (i) {
		return this.cronTime.sendAt(i);
	};

	const start = function () {
		if (this.running) {
			return;
		}

		const MAXDELAY = 2147483647; // The maximum number of milliseconds setTimeout will wait.
		const self = this;
		let timeout = this.cronTime.getTimeout();
		let remaining = 0;
		let startTime;

		if (this.cronTime.realDate) {
			this.runOnce = true;
		}

		function _setTimeout(timeout) {
			startTime = Date.now();
			self._timeout = setTimeout(callbackWrapper, timeout);
			if (self.unrefTimeout && typeof self._timeout.unref === 'function') {
				self._timeout.unref();
			}
		}

		// The callback wrapper checks if it needs to sleep another period or not
		// and does the real callback logic when it's time.
		function callbackWrapper() {
			const diff = startTime + timeout - Date.now();

			if (diff > 0) {
				let newTimeout = self.cronTime.getTimeout();

				if (newTimeout > diff) {
					newTimeout = diff;
				}

				remaining += newTimeout;
			}

			// If there is sleep time remaining, calculate how long and go to sleep
			// again. This processing might make us miss the deadline by a few ms
			// times the number of sleep sessions. Given a MAXDELAY of almost a
			// month, this should be no issue.
			self.lastExecution = new Date();
			if (remaining) {
				if (remaining > MAXDELAY) {
					remaining -= MAXDELAY;
					timeout = MAXDELAY;
				} else {
					timeout = remaining;
					remaining = 0;
				}

				_setTimeout(timeout);
			} else {
				// We have arrived at the correct point in time.

				self.running = false;

				// start before calling back so the callbacks have the ability to stop the cron job
				if (!self.runOnce) {
					self.start();
				}

				self.fireOnTick();
			}
		}

		if (timeout >= 0) {
			this.running = true;

			// Don't try to sleep more than MAXDELAY ms at a time.

			if (timeout > MAXDELAY) {
				remaining = timeout - MAXDELAY;
				timeout = MAXDELAY;
			}

			_setTimeout(timeout);
		} else {
			this.stop();
		}
	};

	CJ.prototype.start = start;

	CJ.prototype.lastDate = function () {
		return this.lastExecution;
	};

	/**
	 * Stop the cronjob.
	 */
	CJ.prototype.stop = function () {
		if (this._timeout) clearTimeout(this._timeout);
		this.running = false;
		if (typeof this.onComplete === 'function') {
			this.onComplete();
		}
	};

	return CJ;
}

module.exports = CronJob;
