text: 'use strict';
  
  const configurationError = require('./configurationError');
  const isSingleLineString = require('./isSingleLineString');
  const isWhitespace = require('./isWhitespace');
  
  /**
   * @typedef {object} Messages
   * @property {function} [expectedBefore]
   * @property {function} [rejectedBefore]
   * @property {function} [expectedAfter]
   * @property {function} [rejectedAfter]
   * @property {function} [expectedBeforeSingleLine]
   * @property {function} [rejectedBeforeSingleLine]
   * @property {function} [expectedBeforeMultiLine]
   * @property {function} [rejectedBeforeMultiLine]
   * @property {function} [expectedAfterSingleLine]
   * @property {function} [rejectedAfterSingleLine]
   * @property {function} [expectedAfterMultiLine]
   * @property {function} [rejectedAfterMultiLine]
   */
  
  /**
   * @typedef {object} WhitespaceCheckerArgs
   * @property {string} source - The source string
   * @property {number} index - The index of the character to check before
   * @property {function} err - If a violation is found, this callback
   *   will be invoked with the relevant warning message.
   *   Typically this callback will report() the violation.
   * @property {function} errTarget - If a violation is found, this string
   *   will be sent to the relevant warning message.
   * @property {string} [lineCheckStr] - Single- and multi-line checkers
   *   will use this string to determine whether they should proceed,
   *   i.e. if this string is one line only, single-line checkers will check,
   *   multi-line checkers will ignore.
   *   If none is passed, they will use `source`.
   * @property {boolean} [onlyOneChar=false] - Only check *one* character before.
   *   By default, "always-*" checks will look for the `targetWhitespace` one
   *   before and then ensure there is no whitespace two before. This option
   *   bypasses that second check.
   * @property {boolean} [allowIndentation=false] - Allow arbitrary indentation
   *   between the `targetWhitespace` (almost definitely a newline) and the `index`.
   *   With this option, the checker will see if a newline *begins* the whitespace before
   *   the `index`.
   */
  
  /**
   * @callback WhitespaceChecker
   * @param {WhitespaceCheckerArgs} args
   */
  
  /**
   * Create a whitespaceChecker, which exposes the following functions:
   * - `before()`
   * - `beforeAllowingIndentation()`
   * - `after()`
   * - `afterOneOnly()`
   *
   * @param {"space" | "newline"} targetWhitespace - This is a keyword instead
   *   of the actual character (e.g. " ") in order to accommodate
   *   different styles of newline ("\n" vs "\r\n")
   * @param { "always" | "never" | "always-single-line" | "always-multi-line" | "never-single-line" | "never-multi-line" } expectation
   * @param {Messages} messages - An object of message functions;
   *   calling `before*()` or `after*()` and the `expectation` that is passed
   *   determines which message functions are required
   *
   * @returns {object} The checker, with its exposed checking functions
   */
  module.exports = function(targetWhitespace, expectation, messages) {
  	// Keep track of active arguments in order to avoid passing
  	// too much stuff around, making signatures long and confusing.
  	// This variable gets reset anytime a checking function is called.
  	/**
  	 * @type {{
  			source?: any,
  			index?: any,
  			err: any,
  			errTarget: any,
  			onlyOneChar: any,
  			allowIndentation?: any,
  		}}
  	*/
  	let activeArgs;
  
  	/**
  	 * Check for whitespace *before* a character.
  	 * @type {WhitespaceChecker}
  	 */
  	function before({
  		source,
  		index,
  		err,
  		errTarget,
  		lineCheckStr,
  		onlyOneChar = false,
  		allowIndentation = false,
  	}) {
  		activeArgs = {
  			source,
  			index,
  			err,
  			errTarget,
  			onlyOneChar,
  			allowIndentation,
  		};
  
  		switch (expectation) {
  			case 'always':
  				expectBefore();
  				break;
  			case 'never':
  				rejectBefore();
  				break;
  			case 'always-single-line':
  				if (!isSingleLineString(lineCheckStr || source)) {
  					return;
  				}
  
  				expectBefore(messages.expectedBeforeSingleLine);
  				break;
  			case 'never-single-line':
  				if (!isSingleLineString(lineCheckStr || source)) {
  					return;
  				}
  
  				rejectBefore(messages.rejectedBeforeSingleLine);
  				break;
  			case 'always-multi-line':
  				if (isSingleLineString(lineCheckStr || source)) {
  					return;
  				}
  
  				expectBefore(messages.expectedBeforeMultiLine);
  				break;
  			case 'never-multi-line':
  				if (isSingleLineString(lineCheckStr || source)) {
  					return;
  				}
  
  				rejectBefore(messages.rejectedBeforeMultiLine);
  				break;
  			default:
  				throw configurationError(`Unknown expectation "${expectation}"`);
  		}
  	}
  
  	/**
  	 * Check for whitespace *after* a character.
  	 * @type {WhitespaceChecker}
  	 */
  	function after({ source, index, err, errTarget, lineCheckStr, onlyOneChar = false }) {
  		activeArgs = { source, index, err, errTarget, onlyOneChar };
  
  		switch (expectation) {
  			case 'always':
  				expectAfter();
  				break;
  			case 'never':
  				rejectAfter();
  				break;
  			case 'always-single-line':
  				if (!isSingleLineString(lineCheckStr || source)) {
  					return;
  				}
  
  				expectAfter(messages.expectedAfterSingleLine);
  				break;
  			case 'never-single-line':
  				if (!isSingleLineString(lineCheckStr || source)) {
  					return;
  				}
  
  				rejectAfter(messages.rejectedAfterSingleLine);
  				break;
  			case 'always-multi-line':
  				if (isSingleLineString(lineCheckStr || source)) {
  					return;
  				}
  
  				expectAfter(messages.expectedAfterMultiLine);
  				break;
  			case 'never-multi-line':
  				if (isSingleLineString(lineCheckStr || source)) {
  					return;
  				}
  
  				rejectAfter(messages.rejectedAfterMultiLine);
  				break;
  			default:
  				throw configurationError(`Unknown expectation "${expectation}"`);
  		}
  	}
  
  	/**
  	 * @param {WhitespaceCheckerArgs} obj
  	 */
  	function beforeAllowingIndentation(obj) {
  		before({ ...obj, allowIndentation: true });
  	}
  
  	/**
  	 * @param {Function} [messageFunc]
  	 */
  	function expectBefore(messageFunc = messages.expectedBefore) {
  		if (activeArgs.allowIndentation) {
  			expectBeforeAllowingIndentation(messageFunc);
  
  			return;
  		}
  
  		const _activeArgs = activeArgs;
  		const source = _activeArgs.source;
  		const index = _activeArgs.index;
  
  		const oneCharBefore = source[index - 1];
  		const twoCharsBefore = source[index - 2];
  
  		if (!isValue(oneCharBefore)) {
  			return;
  		}
  
  		if (targetWhitespace === 'space' && oneCharBefore === ' ') {
  			if (activeArgs.onlyOneChar || !isWhitespace(twoCharsBefore)) {
  				return;
  			}
  		}
  
  		activeArgs.err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
  	}
  
  	/**
  	 * @param {Function} [messageFunc]
  	 */
  	function expectBeforeAllowingIndentation(messageFunc = messages.expectedBefore) {
  		const _activeArgs2 = activeArgs;
  		const source = _activeArgs2.source;
  		const index = _activeArgs2.index;
  		const err = _activeArgs2.err;
  
  		const expectedChar = (function() {
  			if (targetWhitespace === 'newline') {
  				return '\n';
  			}
  		})();
  		let i = index - 1;
  
  		while (source[i] !== expectedChar) {
  			if (source[i] === '\t' || source[i] === ' ') {
  				i--;
  				continue;
  			}
  
  			err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
  
  			return;
  		}
  	}
  
  	/**
  	 * @param {Function} [messageFunc]
  	 */
  	function rejectBefore(messageFunc = messages.rejectedBefore) {
  		const _activeArgs3 = activeArgs;
  		const source = _activeArgs3.source;
  		const index = _activeArgs3.index;
  
  		const oneCharBefore = source[index - 1];
  
  		if (isValue(oneCharBefore) && isWhitespace(oneCharBefore)) {
  			activeArgs.err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
  		}
  	}
  
  	/**
  	 * @param {WhitespaceCheckerArgs} obj
  	 */
  	function afterOneOnly(obj) {
  		after({ ...obj, onlyOneChar: true });
  	}
  
  	/**
  	 * @param {Function} [messageFunc]
  	 */
  	function expectAfter(messageFunc = messages.expectedAfter) {
  		const _activeArgs4 = activeArgs;
  		const source = _activeArgs4.source;
  		const index = _activeArgs4.index;
  
  		const oneCharAfter = source[index + 1];
  		const twoCharsAfter = source[index + 2];
  
  		if (!isValue(oneCharAfter)) {
  			return;
  		}
  
  		if (targetWhitespace === 'newline') {
  			// If index is followed by a Windows CR-LF ...
  			if (oneCharAfter === '\r' && twoCharsAfter === '\n') {
  				if (activeArgs.onlyOneChar || !isWhitespace(source[index + 3])) {
  					return;
  				}
  			}
  
  			// If index is followed by a Unix LF ...
  			if (oneCharAfter === '\n') {
  				if (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter)) {
  					return;
  				}
  			}
  		}
  
  		if (targetWhitespace === 'space' && oneCharAfter === ' ') {
  			if (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter)) {
  				return;
  			}
  		}
  
  		activeArgs.err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
  	}
  
  	/**
  	 * @param {Function} [messageFunc]
  	 */
  	function rejectAfter(messageFunc = messages.rejectedAfter) {
  		const _activeArgs5 = activeArgs;
  		const source = _activeArgs5.source;
  		const index = _activeArgs5.index;
  
  		const oneCharAfter = source[index + 1];
  
  		if (isValue(oneCharAfter) && isWhitespace(oneCharAfter)) {
  			activeArgs.err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
  		}
  	}
  
  	return {
  		before,
  		beforeAllowingIndentation,
  		after,
  		afterOneOnly,
  	};
  };
  
  /**
   * @param {any} x
   */
  function isValue(x) {
  	return x !== undefined && x !== null;
  }
  