/**
 * Simple noinclude / onlyinclude implementation. Strips all tokens in
 * noinclude sections.
 * @module
 */

'use strict';

const TokenHandler = require('./TokenHandler.js');
const { TokenCollector } = require('./TokenCollector.js');
const { KV, TagTk, EndTagTk, SelfclosingTagTk, EOFTk } = require('../../tokens/TokenTypes.js');

/**
 * This helper function will build a meta token in the right way for these
 * tags.
 */
var buildMetaToken = function(manager, tokenName, isEnd, tsr, src) {
	if (isEnd) {
		tokenName += '/End';
	}

	return new SelfclosingTagTk('meta',
		[ new KV('typeof', tokenName) ],
		tsr ? { tsr: tsr, src: manager.frame.srcText.substring(tsr[0], tsr[1]) } : { src: src }
	);
};

var buildStrippedMetaToken = function(manager, tokenName, startDelim, endDelim) {
	var da = startDelim.dataAttribs;
	var tsr0 = da ? da.tsr : null;
	var t0 = tsr0 ? tsr0[0] : null;
	var t1;

	if (endDelim) {
		da = endDelim.dataAttribs || null;
		var tsr1 = da ? da.tsr : null;
		t1 = tsr1 ? tsr1[1] : null;
	} else {
		t1 = manager.frame.srcText.length;
	}

	return buildMetaToken(manager, tokenName, false, [t0, t1]);
};


/**
 * OnlyInclude sadly forces synchronous template processing, as it needs to
 * hold onto all tokens in case an onlyinclude block is encountered later.
 * This can fortunately be worked around by caching the tokens after
 * onlyinclude processing (which is a good idea anyway).
 *
 * @class
 * @extends module:wt2html/tt/TokenHandler
 */
class OnlyInclude extends TokenHandler {
	constructor(manager, options) {
		super(manager, options);
		if (this.options.isInclude) {
			this.accum = [];
			this.inOnlyInclude = false;
			this.foundOnlyInclude = false;
		}
	}

	onAny(token) {
		return this.options.isInclude ? this.onAnyInclude(token) : token;
	}

	onTag(token) {
		return !this.options.isInclude && token.name === 'onlyinclude' ? this.onOnlyInclude(token) : token;
	}

	onOnlyInclude(token) {
		var tsr = token.dataAttribs.tsr;
		var src = !this.options.inTemplate ? token.getWTSource(this.manager.frame) : undefined;
		var attribs = [
			new KV('typeof', 'mw:Includes/OnlyInclude' + (token instanceof EndTagTk ? '/End' : '')),
		];
		var meta = new SelfclosingTagTk('meta', attribs, { tsr: tsr, src: src });
		return { tokens: [ meta ] };
	}

	onAnyInclude(token) {
		var tagName, isTag, meta;

		if (token.constructor === EOFTk) {
			this.inOnlyInclude = false;
			if (this.accum.length && !this.foundOnlyInclude) {
				var res = this.accum;
				res.push(token);
				this.accum = [];
				return { tokens: res };
			} else {
				this.foundOnlyInclude = false;
				this.accum = [];
				return { tokens: [ token ] };
			}
		}

		isTag = token.constructor === TagTk ||
				token.constructor === EndTagTk ||
				token.constructor === SelfclosingTagTk;

		if (isTag) {
			switch (token.name) {
				case 'onlyinclude':
					tagName = 'mw:Includes/OnlyInclude';
					break;
				case 'includeonly':
					tagName = 'mw:Includes/IncludeOnly';
					break;
				case 'noinclude':
					tagName = 'mw:Includes/NoInclude';
					break;
			}
		}

		var mgr = this.manager;
		var curriedBuildMetaToken = function(isEnd, tsr, src) {
			return buildMetaToken(mgr, tagName, isEnd, tsr, src);
		};

		if (isTag && token.name === 'onlyinclude') {
			if (!this.inOnlyInclude) {
				this.foundOnlyInclude = true;
				this.inOnlyInclude = true;
				// wrap collected tokens into meta tag for round-tripping
				meta = curriedBuildMetaToken(token.constructor === EndTagTk, (token.dataAttribs || {}).tsr);
			} else {
				this.inOnlyInclude = false;
				meta = curriedBuildMetaToken(token.constructor === EndTagTk, (token.dataAttribs || {}).tsr);
			}
			return { tokens: [ meta ] };
		} else {
			if (this.inOnlyInclude) {
				return { tokens: [ token ] };
			} else {
				this.accum.push(token);
				return { };
			}
		}
	}
}

/**
 * @class
 * @extends module:wt2html/tt/TokenCollector~TokenCollector
 */
class NoInclude extends TokenCollector {
	TYPE() { return 'tag'; }
	NAME() { return 'noinclude'; }
	TOEND() { return true; }  // Match the end-of-input if </noinclude> is missing.
	ACKEND() { return true; }

	transformation(collection) {
		var start = collection.shift();

		// A stray end tag.
		if (start.constructor === EndTagTk) {
			var meta = buildMetaToken(this.manager, 'mw:Includes/NoInclude', true,
				(start.dataAttribs || {}).tsr);
			return { tokens: [ meta ] };
		}

		// Handle self-closing tag case specially!
		if (start.constructor === SelfclosingTagTk) {
			return (this.options.isInclude) ?
				{ tokens: [] } :
				{ tokens: [ buildMetaToken(this.manager, 'mw:Includes/NoInclude', false, (start.dataAttribs || {}).tsr) ] };
		}

		var tokens = [];
		var end = collection.pop();
		var eof = end.constructor === EOFTk;

		if (!this.options.isInclude) {
			// Content is preserved
			// Add meta tags for open and close
			var manager = this.manager;
			var curriedBuildMetaToken = function(isEnd, tsr, src) {
				return buildMetaToken(manager, 'mw:Includes/NoInclude', isEnd, tsr, src);
			};
			var startTSR = start && start.dataAttribs && start.dataAttribs.tsr;
			var endTSR = end && end.dataAttribs && end.dataAttribs.tsr;
			tokens.push(curriedBuildMetaToken(false, startTSR));
			tokens = tokens.concat(collection);
			if (end && !eof) {
				tokens.push(curriedBuildMetaToken(true, endTSR));
			}
		} else if (!this.options.inTemplate) {
			// Content is stripped
			tokens.push(buildStrippedMetaToken(this.manager,
				'mw:Includes/NoInclude', start, eof ? null : end));
		}

		// Preserve EOF
		if (eof) {
			tokens.push(end);
		}

		return { tokens: tokens };
	}
}

/**
 * @class
 * @extends module:wt2html/tt/TokenCollector~TokenCollector
 */
class IncludeOnly extends TokenCollector {
	TYPE() { return 'tag'; }
	NAME() { return 'includeonly'; }
	TOEND() { return true; }  // Match the end-of-input if </includeonly> is missing.
	ACKEND() { return false; }

	transformation(collection) {
		var start = collection.shift();

		// Handle self-closing tag case specially!
		if (start.constructor === SelfclosingTagTk) {
			var token = buildMetaToken(this.manager, 'mw:Includes/IncludeOnly', false, (start.dataAttribs || {}).tsr);
			if (start.dataAttribs.src) {
				var datamw = JSON.stringify({ src: start.dataAttribs.src });
				token.addAttribute('data-mw', datamw);
			}
			return (this.options.isInclude) ?
				{ tokens: [] } :
				{ tokens: [token] };
		}

		var tokens = [];
		var end = collection.pop();
		var eof = end.constructor === EOFTk;

		if (this.options.isInclude) {
			// Just pass through the full collection including delimiters
			tokens = tokens.concat(collection);
		} else if (!this.options.inTemplate) {
			// Content is stripped
			// Add meta tags for open and close for roundtripping.
			//
			// We can make do entirely with a single meta-tag since
			// there is no real content.  However, we add a dummy end meta-tag
			// so that all <*include*> meta tags show up in open/close pairs
			// and can be handled similarly by downstream handlers.
			var name = 'mw:Includes/IncludeOnly';
			tokens.push(buildStrippedMetaToken(this.manager, name, start, eof ? null : end));

			if (start.dataAttribs.src) {
				var dataMw = JSON.stringify({ src: start.dataAttribs.src });
				tokens[0].addAttribute('data-mw', dataMw);
			}

			if (end && !eof) {
				// This token is just a placeholder for RT purposes. Since the
				// stripped token (above) got the entire tsr value, we are artificially
				// setting the tsr on this node to zero-width to ensure that
				// DSR computation comes out correct.
				var tsr = (end.dataAttribs || { tsr: [null, null] }).tsr;
				tokens.push(buildMetaToken(this.manager, name, true, [tsr[1], tsr[1]], ''));
			}
		}

		// Preserve EOF
		if (eof) {
			tokens.push(end);
		}

		return { tokens: tokens };
	}
}

if (typeof module === "object") {
	module.exports.NoInclude = NoInclude;
	module.exports.IncludeOnly = IncludeOnly;
	module.exports.OnlyInclude = OnlyInclude;
}