'use strict';

/**
 * PRE handling.
 *
 * PRE-handling relies on the following 5-state FSM.
 *
 * States
 * ------
 * ```
 * SOL           -- start-of-line
 *                  (white-space, comments, meta-tags are all SOL transparent)
 * PRE           -- we might need a pre-block
 *                  (if we enter the PRE_COLLECT state)
 * PRE_COLLECT   -- we will need to generate a pre-block and are collecting
 *                  content for it.
 * MULTILINE_PRE -- we might need to extend the pre-block to multiple lines.
 *                  (depending on whether we see a white-space tok or not)
 * IGNORE        -- nothing to do for the rest of the line.
 * ```
 *
 * Transitions
 * -----------
 *
 * In the transition table below, purge is just a shortcut for:
 * "pass on collected tokens to the callback and reset (getResultAndReset)"
 * ```
 * + --------------+-----------------+---------------+--------------------------+
 * | Start state   |     Token       | End state     |  Action                  |
 * + --------------+-----------------+---------------+--------------------------+
 * | SOL           | --- nl      --> | SOL           | purge                    |
 * | SOL           | --- eof     --> | SOL           | purge                    |
 * | SOL           | --- ws      --> | PRE           | save whitespace token(##)|
 * | SOL           | --- sol-tr  --> | SOL           | TOKS << tok              |
 * | SOL           | --- other   --> | IGNORE        | purge                    |
 * + --------------+-----------------+---------------+--------------------------+
 * | PRE           | --- nl      --> | SOL           | purge                    |
 * | PRE           |  html-blk tag   | IGNORE        | purge                    |
 * |               |  wt-table tag   |               |                          |
 * | PRE           | --- eof     --> | SOL           | purge                    |
 * | PRE           | --- sol-tr  --> | PRE           | SOL-TR-TOKS << tok       |
 * | PRE           | --- other   --> | PRE_COLLECT   | TOKS = SOL-TR-TOKS + tok |
 * + --------------+-----------------+---------------+--------------------------+
 * | PRE_COLLECT   | --- nl      --> | MULTILINE_PRE | save nl token            |
 * | PRE_COLLECT   | --- eof     --> | SOL           | gen-pre                  |
 * | PRE_COLLECT   | --- blk tag --> | IGNORE        | gen-prepurge (#)         |
 * | PRE_COLLECT   | --- any     --> | PRE_COLLECT   | TOKS << tok              |
 * + --------------+-----------------+---------------+--------------------------+
 * | MULTILINE_PRE | --- nl      --> | SOL           | gen-pre                  |
 * | MULTILINE_PRE | --- eof     --> | SOL           | gen-pre                  |
 * | MULTILINE_PRE | --- ws      --> | PRE_COLLECT   | pop saved nl token (##)  |
 * |               |                 |               | TOKS = SOL-TR-TOKS + tok |
 * | MULTILINE_PRE | --- sol-tr  --> | MULTILINE_PRE | SOL-TR-TOKS << tok       |
 * | MULTILINE_PRE | --- any     --> | IGNORE        | gen-pre                  |
 * + --------------+-----------------+---------------+--------------------------+
 * | IGNORE        | --- nl      --> | SOL           | purge                    |
 * | IGNORE        | --- eof     --> | SOL           | purge                    |
 * + --------------+-----------------+---------------+--------------------------+
 *
 * # If we've collected any tokens from previous lines, generate a pre. This
 * line gets purged.
 *
 * ## In these states, check if the whitespace token is a single space or has
 * additional chars (white-space or non-whitespace) -- if yes, slice it off
 * and pass it through the FSM.
 */

const { TokenUtils } = require('../../utils/TokenUtils.js');
const TokenHandler = require('./TokenHandler.js');
const { WTUtils } = require('../../utils/WTUtils.js');
const { TagTk, EndTagTk, SelfclosingTagTk, NlTk, CommentTk } = require('../../tokens/TokenTypes.js');

/**
 * @class
 * @extends module:wt2html/tt/TokenHandler
 */
class PreHandler extends TokenHandler {
	// FSM states
	static STATE_SOL() { return 1; }
	static STATE_PRE() { return  2; }
	static STATE_PRE_COLLECT() { return  3; }
	static STATE_MULTILINE_PRE() { return  4; }
	static STATE_IGNORE() { return  5; }

	// debug string output of FSM states
	static STATE_STR() {
		return {
			1: 'sol        ',
			2: 'pre        ',
			3: 'pre_collect',
			4: 'multiline  ',
			5: 'ignore     ',
		};
	}

	constructor(manager, options) {
		super(manager, options);
		if (this.options.inlineContext || this.options.inPHPBlock) {
			this.disabled = true;
		} else {
			this.disabled = false;
			this.resetState();
		}
	}

	resetState() {
		this.reset();
	}

	reset() {
		this.state  = PreHandler.STATE_SOL();
		this.lastNlTk = null;
		// Initialize to zero to deal with indent-pre
		// on the very first line where there is no
		// preceding newline to initialize this.
		this.preTSR = 0;
		this.tokens = [];
		this.preCollectCurrentLine = [];
		this.preWSToken = null;
		this.multiLinePreWSToken = null;
		this.solTransparentTokens = [];
		this.onAnyEnabled = true;
	}

	moveToIgnoreState() {
		this.onAnyEnabled = false;
		this.state = PreHandler.STATE_IGNORE();
	}

	popLastNL(ret) {
		if (this.lastNlTk) {
			ret.push(this.lastNlTk);
			this.lastNlTk = null;
		}
	}

	resetPreCollectCurrentLine() {
		if (this.preCollectCurrentLine.length > 0) {
			this.tokens = this.tokens.concat(this.preCollectCurrentLine);
			this.preCollectCurrentLine = [];
			// Since the multi-line pre materialized, the multilinee-pre-ws token
			// should be discarded so that it is not emitted after <pre>..</pre>
			// is generated (see processPre).
			this.multiLinePreWSToken = null;
		}
	}

	encounteredBlockWhileCollecting(token) {
		var env = this.manager.env;
		var ret = [];
		var mlp = null;

		// we remove any possible multiline ws token here and save it because
		// otherwise the propressPre below would add it in the wrong place
		if (this.multiLinePreWSToken) {
			mlp = this.multiLinePreWSToken;
			this.multiLinePreWSToken = null;
		}

		if (this.tokens.length > 0) {
			var i = this.tokens.length - 1;
			while (i > 0 && TokenUtils.isSolTransparent(env, this.tokens[i])) { i--; }
			var solToks = this.tokens.splice(i);
			this.lastNlTk = solToks.shift();
			console.assert(this.lastNlTk && this.lastNlTk.constructor === NlTk);
			ret = this.processPre(null).concat(solToks);
		}

		if (this.preWSToken || mlp) {
			ret.push(this.preWSToken || mlp);
			this.preWSToken = null;
		}

		this.resetPreCollectCurrentLine();
		ret = ret.concat(this.getResultAndReset(token));
		return ret;
	}

	getResultAndReset(token) {
		this.popLastNL(this.tokens);

		var ret = this.tokens;
		if (this.preWSToken) {
			ret.push(this.preWSToken);
			this.preWSToken = null;
		}
		if (this.solTransparentTokens.length > 0) {
			ret = ret.concat(this.solTransparentTokens);
			this.solTransparentTokens = [];
		}
		ret.push(token);
		this.tokens = [];
		this.multiLinePreWSToken = null;

		return ret;
	}

	processPre(token) {
		var ret = [];

		// pre only if we have tokens to enclose
		if (this.tokens.length > 0) {
			var da = null;
			if (this.preTSR !== -1) {
				da = { tsr: [this.preTSR, this.preTSR + 1] };
			}
			ret = [new TagTk('pre', [], da)].concat(this.tokens, new EndTagTk('pre'));
		}

		// emit multiline-pre WS token
		if (this.multiLinePreWSToken) {
			ret.push(this.multiLinePreWSToken);
			this.multiLinePreWSToken = null;
		}
		this.popLastNL(ret);

		// sol-transparent toks
		ret = ret.concat(this.solTransparentTokens);

		// push the the current token
		if (token !== null) {
			ret.push(token);
		}

		// reset!
		this.solTransparentTokens = [];
		this.tokens = [];

		return ret;
	}

	onNewline(token) {
		var env = this.manager.env;

		function initPreTSR(nltk) {
			var da = nltk.dataAttribs;
			// tsr[1] can never be zero, so safe to use da.tsr[1] to check for null/undefined
			return (da && da.tsr && da.tsr[1]) ? da.tsr[1] : -1;
		}

		env.log("trace/pre", this.manager.pipelineId, "NL    |",
			PreHandler.STATE_STR()[this.state], "|", function() { return JSON.stringify(token); });

		// Whenever we move into SOL-state, init preTSR to
		// the newline's tsr[1].  This will later be  used
		// to assign 'tsr' values to the <pre> token.

		var ret = [];
		// See TokenHandler's documentation for the onAny handler
		// for what this flag is about.
		var skipOnAny = false;
		switch (this.state) {
			case PreHandler.STATE_SOL():
				ret = this.getResultAndReset(token);
				skipOnAny = true;
				this.preTSR = initPreTSR(token);
				break;

			case PreHandler.STATE_PRE():
				ret = this.getResultAndReset(token);
				skipOnAny = true;
				this.preTSR = initPreTSR(token);
				this.state = PreHandler.STATE_SOL();
				break;

			case PreHandler.STATE_PRE_COLLECT():
				this.resetPreCollectCurrentLine();
				this.lastNlTk = token;
				this.state = PreHandler.STATE_MULTILINE_PRE();
				break;

			case PreHandler.STATE_MULTILINE_PRE():
				this.preWSToken = null;
				this.multiLinePreWSToken = null;
				ret = this.processPre(token);
				skipOnAny = true;
				this.preTSR = initPreTSR(token);
				this.state = PreHandler.STATE_SOL();
				break;

			case PreHandler.STATE_IGNORE():
				ret = [token];
				skipOnAny = true;
				this.reset();
				this.preTSR = initPreTSR(token);
				break;
		}

		env.log("debug/pre", this.manager.pipelineId, "saved :", this.tokens);
		env.log("debug/pre", this.manager.pipelineId, "---->  ",
			function() { return JSON.stringify(ret); });

		return { tokens: ret, skipOnAny: skipOnAny };
	}

	onEnd(token) {
		this.manager.env.log("trace/pre", this.manager.pipelineId, "eof   |",
			PreHandler.STATE_STR()[this.state], "|", function() { return JSON.stringify(token); });

		var ret = [];
		switch (this.state) {
			case PreHandler.STATE_SOL():
			case PreHandler.STATE_PRE():
				ret = this.getResultAndReset(token);
				break;

			case PreHandler.STATE_PRE_COLLECT():
			case PreHandler.STATE_MULTILINE_PRE():
				this.preWSToken = null;
				this.multiLinePreWSToken = null;
				this.resetPreCollectCurrentLine();
				ret = this.processPre(token);
				break;

			case PreHandler.STATE_IGNORE():
				ret.push(token);
				break;
		}

		this.manager.env.log("debug/pre", this.manager.pipelineId, "saved :", this.tokens);
		this.manager.env.log("debug/pre", this.manager.pipelineId, "---->  ",
			function() { return JSON.stringify(ret); });

		return { tokens: ret, skipOnAny: true };
	}

	getUpdatedPreTSR(tsr, token) {
		var tc = token.constructor;
		if (tc === CommentTk) {
			// comment length has 7 added for "<!--" and "-->" deliminters
			// (see WTUtils.decodedCommentLength() -- but that takes a node not a token)
			tsr = token.dataAttribs.tsr ? token.dataAttribs.tsr[1] : (tsr === -1 ? -1 : WTUtils.decodeComment(token.value).length + 7 + tsr);
		} else if (tc === SelfclosingTagTk) {
			// meta-tag (cannot compute)
			tsr = -1;
		} else if (tsr !== -1) {
			// string
			tsr += token.length;
		}
		return tsr;
	}

	onAny(token) {
		var env = this.manager.env;

		env.log("trace/pre", this.manager.pipelineId, "any   |", this.state, ":",
			PreHandler.STATE_STR()[this.state], "|", function() { return JSON.stringify(token); });

		if (this.state === PreHandler.STATE_IGNORE()) {
			env.log("error", function() {
				return "!ERROR! IGNORE! Cannot get here: " + JSON.stringify(token);
			});
			return token;
		}

		var ret = [];
		var tc = token.constructor;
		switch (this.state) {
			case PreHandler.STATE_SOL():
				if ((tc === String) && token.match(/^ /)) {
					ret = this.tokens;
					this.tokens = [];
					this.preWSToken = token[0];
					this.state = PreHandler.STATE_PRE();
					if (!token.match(/^ $/)) {
						// Treat everything after the first space
						// as a new token
						this.onAny(token.slice(1));
					}
				} else if (TokenUtils.isSolTransparent(env, token)) {
					// continue watching ...
					// update pre-tsr since we haven't transitioned to PRE yet
					this.preTSR = this.getUpdatedPreTSR(this.preTSR, token);
					this.tokens.push(token);
				} else {
					ret = this.getResultAndReset(token);
					this.moveToIgnoreState();
				}
				break;

			case PreHandler.STATE_PRE():
				if (TokenUtils.isSolTransparent(env, token)) { // continue watching
					this.solTransparentTokens.push(token);
				} else if (TokenUtils.isTableTag(token) ||
					(TokenUtils.isHTMLTag(token) && TokenUtils.isBlockTag(token.name))) {
					ret = this.getResultAndReset(token);
					this.moveToIgnoreState();
				} else {
					this.preCollectCurrentLine = this.solTransparentTokens.concat(token);
					this.solTransparentTokens = [];
					this.state = PreHandler.STATE_PRE_COLLECT();
				}
				break;

			case PreHandler.STATE_PRE_COLLECT():
				if (token.name && TokenUtils.isBlockTag(token.name)) {
					ret = this.encounteredBlockWhileCollecting(token);
					this.moveToIgnoreState();
				} else {
					// nothing to do .. keep collecting!
					this.preCollectCurrentLine.push(token);
				}
				break;

			case PreHandler.STATE_MULTILINE_PRE():
				if ((tc === String) && token.match(/^ /)) {
					this.popLastNL(this.tokens);
					this.state = PreHandler.STATE_PRE_COLLECT();
					this.preWSToken = null;

					// Pop buffered sol-transparent tokens
					this.tokens = this.tokens.concat(this.solTransparentTokens);
					this.solTransparentTokens = [];

					// check if token is single-space or more
					this.multiLinePreWSToken = token[0];
					if (!token.match(/^ $/)) {
						// Treat everything after the first space as a new token
						this.onAny(token.slice(1));
					}
				} else if (TokenUtils.isSolTransparent(env, token)) { // continue watching
					this.solTransparentTokens.push(token);
				} else {
					ret = this.processPre(token);
					this.moveToIgnoreState();
				}
				break;
		}

		env.log("debug/pre", this.manager.pipelineId, "saved :", this.tokens);
		env.log("debug/pre", this.manager.pipelineId, "---->  ",
			function() { return JSON.stringify(ret); });

		return { tokens: ret };
	}
}

if (typeof module === "object") {
	module.exports.PreHandler = PreHandler;
}