/** @module */

'use strict';

const { JSUtils } = require('../../utils/jsutils.js');
const { NlTk, EOFTk } = require('../../tokens/TokenTypes.js');

/**
 * @class
 */
module.exports = class TokenHandler {
	/**
	 * @param {TokenTransformManager} manager
	 *   The manager for this stage of the parse.
	 * @param {Object} options
	 *   Any options for the expander.
	 */
	constructor(manager, options) {
		this.manager = manager;
		this.env = manager.env;
		this.options = options;
		this.atTopLevel = false;

		// This is set if the token handler is disabled for the entire pipeline.
		this.disabled = false;

		// This is set/reset by the token handlers at various points
		// in the token stream based on what is encountered.
		// This only enables/disables the onAny handler.
		this.onAnyEnabled = true;
	}

	/**
	 * This handler is called for EOF tokens only
	 * @param {EOFTk} token EOF token to be processed
	 * @return {Object}
	 *    return value can be one of 'token'
	 *    or { tokens: [..] }
	 *    or { tokens: [..], skipOnAny: .. }
	 *    if 'skipOnAny' is set, onAny handler is skipped
	 */
	onEnd(token) { return token; }

	/**
	 * This handler is called for newline tokens only
	 * @param {NlTk} token Newline token to be processed
	 * @return {Object}
	 *    return value can be one of 'token'
	 *    or { tokens: [..] }
	 *    or { tokens: [..], skipOnAny: .. }
	 *    if 'skipOnAny' is set, onAny handler is skipped
	 */
	onNewline(token) { return token; }

	/**
	 * This handler is called for tokens that are not EOFTk or NLTk tokens.
	 * The handler may choose to process only specific kinds of tokens.
	 * For example, a list handler may only process 'listitem' TagTk tokens.
	 *
	 * @param {Token} token Token to be processed
	 * @return {Object}
	 *    return value can be one of 'token'
	 *    or { tokens: [..] }
	 *    or { tokens: [..], skipOnAny: .. }
	 *    if 'skipOnAny' is set, onAny handler is skipped
	 */
	onTag(token) { return token; }

	/**
	 * This handler is called for *all* tokens in the token stream except if
	 * (a) The more specific handlers above modified the token
	 * (b) the more specific handlers (onTag, onEnd, onNewline) have set
	 *     the skip flag in their return values.
	 * (c) this handlers 'onAnyEnabled' flag is set to false (can be set by any
	 *     of the handlers).
	 *
	 * @param {Token} token Token to be processed
	 * @return {Object}
	 *    return value can be one of 'token'
	 *    or { tokens: [..] }
	 *    or { tokens: [..], skipOnAny: .. }
	 */
	onAny(token) { return token; }

	/**
	 * Reset pipeline state (since pipelines are shared)
	 * @param {Object} opts Reset options
	 */
	resetState(opts) { this.atTopLevel = opts && opts.toplevel; }

	/* -------------------------- PORT-FIXME ------------------------------
	 * Once ported to PHP, we should benchmark a version of this function
	 * without any of the tracing code in it. There are upto 4 untaken branches
	 * that are executed in the hot loop for every single token. Unlike V8,
	 * this code will not be JIT-ted to eliminate that overhead.
	 *
	 * In the common case where tokens come through functions unmodified
	 * because of hitting default identity handlers, these 4 extra branches
	 * could potentially amount to something. That might be partially ameliorated
	 * by the fact that most modern processors have branch prediction and these
	 * branches will always fail and so might not be such a big deal.
	 *
	 * In any case, worth a performance test after the port.
	 * -------------------------------------------------------------------- */
	/**
	 * Push an input array of tokens through the transformer
	 * and return the transformed tokens
	 *
	 * @param {Object} env Parser Environment
	 * @param {Array} tokens The array of tokens to process
	 * @param {Object} traceState Tracing related state
	 * @return {Array} the array of transformed tokens
	 */
	processTokensSync(env, tokens, traceState) {
		const traceFlags = traceState && traceState.traceFlags;
		const traceTime = traceState && traceState.traceTime;
		let accum = [];
		while (tokens.length > 0) {
			const token = tokens.shift();

			if (traceFlags) {
				traceState.tracer(token, this);
			}

			let res, resTokens;
			if (traceTime) {
				const s = JSUtils.startTime();
				let traceName;
				if (token.constructor === NlTk) {
					res = this.onNewline(token);
					traceName = traceState.traceNames[0];
				} else if (token.constructor === EOFTk) {
					res = this.onEnd(token);
					traceName = traceState.traceNames[1];
				} else if (token.constructor !== String) {
					res = this.onTag(token);
					traceName = traceState.traceNames[2];
				} else {
					res = token;
				}
				if (traceName) {
					const t = JSUtils.elapsedTime(s);
					env.bumpTimeUse(traceName, t, "TT");
					env.bumpCount(traceName);
					traceState.tokenTimes += t;
				}
			} else {
				if (token.constructor === NlTk) {
					res = this.onNewline(token);
				} else if (token.constructor === EOFTk) {
					res = this.onEnd(token);
				} else if (token.constructor !== String) {
					res = this.onTag(token);
				} else {
					res = token;
				}
			}

			let modified = false;
			if (res !== token &&
				(!res.tokens || res.tokens.length !== 1 || res.tokens[0] !== token)
			) {
				resTokens = res.tokens;
				modified = true;
			}

			if (!modified && !res.skipOnAny && this.onAnyEnabled) {
				if (traceTime) {
					const s = JSUtils.startTime();
					const traceName = traceState.traceNames[3];
					res = this.onAny(token);
					const t = JSUtils.elapsedTime(s);
					env.bumpTimeUse(traceName, t, "TT");
					env.bumpCount(traceName);
					traceState.tokenTimes += t;
				} else {
					res = this.onAny(token);
				}
				if (res !== token &&
					(!res.tokens || res.tokens.length !== 1 || res.tokens[0] !== token)
				) {
					resTokens = res.tokens;
					modified = true;
				}
			}

			if (!modified) {
				accum.push(token);
			} else if (resTokens && resTokens.length) {
				accum = accum.concat(resTokens);
			}
		}

		return accum;
	}
};