/** @module wt2html/Frame */

'use strict';

require('../../core-upgrade.js');

const { Params } = require('./Params.js');

const { EOFTk } = require('../tokens/TokenTypes.js');

const { JSUtils } = require('../utils/jsutils.js');
const { PipelineUtils } = require('../utils/PipelineUtils.js');
const { TokenUtils } = require('../utils/TokenUtils.js');
const { Util } = require('../utils/Util.js');

/**
 * @class
 *
 * The Frame object
 *
 * A frame represents a template expansion scope including parameters passed
 * to the template (args). It provides a generic 'expand' method which
 * expands / converts individual parameter values in its scope.  It also
 * provides methods to check if another expansion would lead to loops or
 * exceed the maximum expansion depth.
 */
class Frame {
	constructor(title, env, args, srcText, parentFrame) {
		this.title = title;
		this.env = env;
		this.args = new Params(args);
		this.srcText = srcText;
		console.assert(typeof (srcText) === 'string');

		if (parentFrame) {
			this.parentFrame = parentFrame;
			this.depth = parentFrame.depth + 1;
		} else {
			this.parentFrame = null;
			this.depth = 0;
		}
	}

	/**
	 * Create a new child frame.
	 */
	newChild(title, args, srcText) {
		return new Frame(title, this.env, args, srcText, this);
	}

	/**
	 * Expand / convert a thunk (a chunk of tokens not yet fully expanded).
	 *
	 * XXX: Support different input formats, expansion phases / flags and more
	 * output formats.
	 *
	 * @return {Promise} A promise which will be resolved with the expanded
	 *  chunk of tokens.
	 */
	expand(chunk, options) {
		const outType = options.type;
		console.assert(outType === 'tokens/x-mediawiki/expanded', "Expected tokens/x-mediawiki/expanded type");
		this.env.log('debug', 'Frame.expand', chunk);

		const cb = JSUtils.mkPromised(
			options.cb
			// XXX ignores the `err` parameter in callback.  This isn't great!
				? function(err, val) { options.cb(val); } // eslint-disable-line handle-callback-err
				: undefined
		);
		if (!chunk.length || chunk.constructor === String) {
			// Nothing to do
			cb(null, chunk);
			return cb.promise;
		}

		if (options.asyncCB) {
			// Signal (potentially) asynchronous expansion to parent.
			options.asyncCB({ async: true });
		}

		// Downstream template uses should be tracked and wrapped only if:
		// - not in a nested template        Ex: {{Templ:Foo}} and we are processing Foo
		// - not in a template use context   Ex: {{ .. | {{ here }} | .. }}
		// - the attribute use is wrappable  Ex: [[ ... | {{ .. link text }} ]]

		const opts = {
			// XXX: use input type
			pipelineType: 'tokens/x-mediawiki',
			pipelineOpts: {
				isInclude: this.depth > 0,
				expandTemplates: options.expandTemplates,
				inTemplate: options.inTemplate,
			},
			sol: true,
			srcOffsets: options.srcOffsets,
		};

		// In the name of interface simplicity, we accumulate all emitted
		// chunks in a single accumulator.
		const eventState = { options: options, accum: [], cb: cb };
		opts.chunkCB = this.onThunkEvent.bind(this, eventState, true);
		opts.endCB = this.onThunkEvent.bind(this, eventState, false);
		opts.tplArgs = { name: null, title: null, attribs: [] };

		const content = chunk;
		if (JSUtils.lastItem(chunk).constructor !== EOFTk) {
			content.push(new EOFTk());
		}

		// XXX should use `PipelineUtils#promiseToProcessContent` for better error handling.
		PipelineUtils.processContentInPipeline(this.env, this, content, opts);
		return cb.promise;
	}

	/**
	 * Event handler for chunk conversion pipelines.
	 * @private
	 */
	onThunkEvent(state, notYetDone, ret) {
		if (notYetDone) {
			state.accum = JSUtils.pushArray(state.accum, TokenUtils.stripEOFTkfromTokens(ret));
			this.env.log('debug', 'Frame.onThunkEvent accum:', state.accum);
		} else {
			this.env.log('debug', 'Frame.onThunkEvent:', state.accum);
			state.cb(null, state.accum);
		}
	}

	/**
	 * Check if expanding a template would lead to a loop, or would exceed the
	 * maximum expansion depth.
	 *
	 * @param {string} title
	 */
	loopAndDepthCheck(title, maxDepth, ignoreLoop) {
		if (this.depth > maxDepth) {
			// Too deep
			return `Template recursion depth limit exceeded (${maxDepth}): `;
		}
		if (ignoreLoop) { return false; }
		let elem = this;
		do {
			if (title && elem.title && Util.titleEquals(elem.title, title)) {
				// Loop detected
				return 'Template loop detected: ';
			}
			elem = elem.parentFrame;
		} while (elem);
		// No loop detected.
		return false;
	}

	_getID(options) {
		if (!options || !options.cb) {
			console.trace();
			console.warn('Error in Frame._getID: no cb in options!');
		} else {
			return options.cb(this);
		}
	}
}

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