/**
 * @typedef GetOptions
 * @property {string} [split] Characters to split on
 * @property {string} [mode] "until" or "between"; choose where to get the content from
 * @property {function} [transform] Transformation to apply to result before returning
 */

const tf = {
	lc: s => s.toLowerCase()
}

class Parser {
	constructor(string) {
		this.string = string;
		this.substore = [];
		this.cursor = 0;
		this.cursorStore = [];
		this.mode = "until";
		this.transform = s => s;
		this.split = " ";
	}

	/**
	 * Return all the remaining text from the buffer, without updating the cursor
	 * @return {string}
	 */
	remaining() {
		return this.string.slice(this.cursor);
	}

	/**
	 * Have we reached the end of the string yet?
	 * @return {boolean}
	 */
	hasRemaining() {
		return this.cursor < this.string.length
	}

	/**
	 * Get the next element from the buffer, either up to a token or between two tokens, and update the cursor.
	 * @param {GetOptions} [options]
	 * @returns {string}
	 */
	get(options = {}) {
		["mode", "split", "transform"].forEach(o => {
			if (!options[o]) options[o] = this[o];
		});
		if (options.mode == "until") {
			let next = this.string.indexOf(options.split, this.cursor+options.split.length-1);
			if (next == -1) {
				let result = this.remaining();
				this.cursor = this.string.length;
				return result;
			} else {
				let result = this.string.slice(this.cursor, next);
				this.cursor = next + options.split.length;
				return options.transform(result);
			}
		} else if (options.mode == "between") {
			let start = this.string.indexOf(options.split, this.cursor);
			let end = this.string.indexOf(options.split, start+options.split.length);
			let result = this.string.slice(start+options.split.length, end);
			this.cursor = end + options.split.length;
			return options.transform(result);
		}
	}

	/**
	 * Get a number of chars from the buffer.
	 * @param {number} length Number of chars to get
	 * @param {boolean} [move] Whether to update the cursor
	 */
	slice(length, move = false) {
		let result = this.string.slice(this.cursor, this.cursor+length);
		if (move) this.cursor += length;
		return result;
	}

	/**
	 * Repeatedly swallow a character.
	 * @param {string} char
	 */
	swallow(char) {
		let before = this.cursor;
		while (this.string[this.cursor] == char) this.cursor++;
		return this.cursor - before;
	}

	/**
	 * Push the current cursor position to the store
	 */
	store() {
		this.cursorStore.push(this.cursor);
	}

	/**
	 * Pop the previous cursor position from the store
	 */
	restore() {
		this.cursor = this.cursorStore.pop();
	}

	/**
	 * Run a get operation, test against an input, return success or failure, and restore the cursor.
	 * @param {string} value The value to test against
	 * @param {object} options Options for get
	 */
	test(value, options) {
		this.store();
		let next = this.get(options);
		let result = next == value;
		this.restore();
		return result;
	}

	/**
	 * Run a get operation, test against an input, return success or failure, and restore the cursor on failure.
	 * @param {string} value The value to test against
	 * @param {object} options Options for get
	 */
	has(value, options) {
		this.store();
		let next = this.get(options);
		let result = next == value;
		if (!result) this.restore();
		return result;
	}

	/**
	 * Run a get operation, test against an input, and throw an error if it doesn't match.
	 * @param {string} value
	 * @param {GetOptions} [options]
	 */
	expect(value, options = {}) {
		let next = this.get(options);
		if (next != value) throw new Error("Expected "+value+", got "+next);
	}

	/**
	 * Seek to or past the next occurance of the string.
	 * @param {string} toFind
	 * @param {{moveToMatch?: boolean, useEnd?: boolean}} options both default to false
	 */
	seek(toFind, options = {}) {
		if (options.moveToMatch === undefined) options.moveToMatch = false
		if (options.useEnd === undefined) options.useEnd = false
		let index = this.string.indexOf(toFind, this.cursor)
		if (index !== -1) {
			index -= this.cursor
			if (options.useEnd) index += toFind.length
			if (options.moveToMatch) this.cursor += index
		}
		return index
	}

	/**
	 * Replace the current string, adding the old one to the substore.
	 * @param {string} string
	 */
	pushSubstore(string) {
		this.substore.push({string: this.string, cursor: this.cursor, cursorStore: this.cursorStore})
		this.string = string
		this.cursor = 0
		this.cursorStore = []
	}

	/**
	 * Replace the current string with the first entry from the substore.
	 */
	popSubstore() {
		Object.assign(this, this.substore.pop())
	}
}

module.exports.Parser = Parser
