/** @module */

"use strict";

const Consts = require('../../config/WikitextConstants.js').WikitextConstants;
const { ContentUtils } = require('../../utils/ContentUtils.js');
const { DOMUtils } = require('../../utils/DOMUtils.js');
const { JSUtils } = require('../../utils/jsutils.js');
const { PipelineUtils } = require('../../utils/PipelineUtils.js');
const Promise = require('../../utils/promise.js');
const TokenHandler = require('./TokenHandler.js');
const { KV, EOFTk, TagTk, EndTagTk } = require('../../tokens/TokenTypes.js');

/**
 * Handler for language conversion markup, which looks like `-{ ... }-`.
 *
 * @class
 * @extends module:wt2html/tt/TokenHandler
 */
class LanguageVariantHandler extends TokenHandler {
	/**
	 * @param {Object} manager
	 * @param {Object} options
	 */
	constructor(manager, options) {
		super(manager, options);
		this.manager.addTransformP(
			this, this.onLanguageVariant,
			"LanguageVariantHandler:onLanguageVariant",
			LanguageVariantHandler.rank(), 'tag', 'language-variant'
		);
	}

	// Indicates where in the pipeline this handler should be run.
	static rank() { return 1.16; }

	/**
	 * Main handler.
	 * See {@link TokenTransformManager#addTransform}'s transformation parameter
	 */
	*onLanguageVariantG(token) {
		const manager = this.manager;
		const options = this.options;
		const attribs = token.attribs;
		const dataAttribs = token.dataAttribs;
		const tsr = dataAttribs.tsr;
		const flags = dataAttribs.flags || [];
		let flagSp = dataAttribs.flagSp;
		let isMeta = false;
		let sawFlagA = false;

		// convert one variant text to dom.
		const convertOne = Promise.async(function *(t) {
			// we're going to fetch the actual token list from attribs
			// (this ensures that it has gone through the earlier stages
			// of the pipeline already to be expanded)
			t = +(t.replace(/^mw:lv/, ''));
			const srcOffsets = attribs[t].srcOffsets;
			const doc = yield PipelineUtils.promiseToProcessContent(
				manager.env, manager.frame, attribs[t].v.concat([new EOFTk()]),
				{
					pipelineType: 'tokens/x-mediawiki/expanded',
					pipelineOpts: {
						inlineContext: true,
						expandTemplates: options.expandTemplates,
						inTemplate: options.inTemplate,
					},
					srcOffsets: srcOffsets ? srcOffsets.slice(2,4) : undefined,
					sol: true,
				}
			);
			return {
				xmlstr: ContentUtils.ppToXML(doc.body, { innerXML: true }),
				isBlock: DOMUtils.hasBlockElementDescendant(doc.body)
			};
		});
		// compress a whitespace sequence
		const compressSpArray = function(a) {
			const result = [];
			let ctr = 0;
			if (a === undefined) {
				return a;
			}
			a.forEach(function(sp) {
				if (sp === '') {
					ctr++;
				} else {
					if (ctr > 0) {
						result.push(ctr);
						ctr = 0;
					}
					result.push(sp);
				}
			});
			if (ctr > 0) { result.push(ctr); }
			return result;
		};
		// remove trailing semicolon marker, if present
		let trailingSemi = false;
		if (
			dataAttribs.texts.length &&
				dataAttribs.texts[dataAttribs.texts.length - 1].semi
		) {
			trailingSemi = dataAttribs.texts.pop().sp;
		}
		// convert all variant texts to DOM
		let isBlock = false;
		const texts = yield Promise.map(dataAttribs.texts, Promise.async(function *(t) {
			let text, from, to;
			if (t.twoway) {
				text = yield convertOne(t.text);
				isBlock = isBlock || text.isBlock;
				return { lang: t.lang, text: text.xmlstr, twoway: true, sp: t.sp };
			} else if (t.lang) {
				from = yield convertOne(t.from);
				to = yield convertOne(t.to);
				isBlock = isBlock || from.isBlock || to.isBlock;
				return { lang: t.lang, from: from.xmlstr, to: to.xmlstr, sp: t.sp };
			} else {
				text = yield convertOne(t.text);
				isBlock = isBlock || text.isBlock;
				return { text: text.xmlstr, sp: [] };
			}
		}));
		// collect two-way/one-way conversion rules
		const oneway = [];
		let twoway = [];
		let sawTwoway = false;
		let sawOneway = false;
		let textSp;
		const twowaySp = [];
		const onewaySp = [];
		texts.forEach((t) => {
			if (t.twoway) {
				twoway.push({ l: t.lang, t: t.text });
				twowaySp.push(t.sp[0], t.sp[1], t.sp[2]);
				sawTwoway = true;
			} else if (t.lang) {
				oneway.push({ l: t.lang, f: t.from, t: t.to });
				onewaySp.push(t.sp[0], t.sp[1], t.sp[2], t.sp[3]);
				sawOneway = true;
			}
		});

		// To avoid too much data-mw bloat, only the top level keys in
		// data-mw-variant are "human readable".  Nested keys are single-letter:
		// `l` for `language`, `t` for `text` or `to`, `f` for `from`.
		let dataMWV;
		if (flags.length === 0 && dataAttribs.variants.length > 0) {
			// "Restrict possible variants to a limited set"
			dataMWV = {
				filter: { l: dataAttribs.variants, t: texts[0].text },
				show: true
			};
		} else {
			dataMWV = flags.reduce(function(dmwv, f) {
				if (Consts.LCFlagMap.has(f)) {
					if (Consts.LCFlagMap.get(f)) {
						dmwv[Consts.LCFlagMap.get(f)] = true;
						if (f === 'A') {
							sawFlagA = true;
						}
					}
				} else {
					dmwv.error = true;
				}
				return dmwv;
			}, {});
			// (this test is done at the top of ConverterRule::getRuleConvertedStr)
			// (also partially in ConverterRule::parse)
			if (texts.length === 1 && !texts[0].lang  && !dataMWV.name) {
				if (dataMWV.add || dataMWV.remove) {
					const variants = [ '*' ];
					twoway = variants.map(function(code) {
						return { l: code, t: texts[0].text };
					});
					sawTwoway = true;
				} else {
					dataMWV.disabled = true;
					dataMWV.describe = undefined;
				}
			}
			if (dataMWV.describe) {
				if (!sawFlagA) { dataMWV.show = true; }
			}
			if (dataMWV.disabled || dataMWV.name) {
				if (dataMWV.disabled) {
					dataMWV.disabled = { t: texts[0].text };
				} else {
					dataMWV.name = { t: texts[0].text };
				}
				dataMWV.show =
					(dataMWV.title || dataMWV.add) ? undefined : true;
			} else if (sawTwoway) {
				dataMWV.twoway = twoway;
				textSp = twowaySp;
				if (sawOneway) { dataMWV.error = true; }
			} else {
				dataMWV.oneway = oneway;
				textSp = onewaySp;
				if (!sawOneway) { dataMWV.error = true; }
			}
		}
		// Use meta/not meta instead of explicit 'show' flag.
		isMeta = !dataMWV.show;
		dataMWV.show = undefined;
		// Trim some data from data-parsoid if it matches the defaults
		if (flagSp.length === 2 * dataAttribs.original.length) {
			if (flagSp.every(function(s) { return s === ''; })) {
				flagSp = undefined;
			}
		}
		if (trailingSemi !== false && textSp) {
			textSp.push(trailingSemi);
		}

		// Our markup is always the same, except for the contents of
		// the data-mw-variant attribute and whether it's a span, div, or a
		// meta, depending on (respectively) whether conversion output
		// contains only inline content, could contain block content,
		// or never contains any content.
		const tokens = [
			new TagTk(isMeta ? 'meta' : isBlock ? 'div' : 'span', [
				new KV('typeof', 'mw:LanguageVariant'),
				new KV(
					'data-mw-variant',
					JSON.stringify(JSUtils.sortObject(dataMWV))
				),
			], {
				fl: dataAttribs.original, // original "fl"ags
				flSp: compressSpArray(flagSp), // spaces around flags
				src: dataAttribs.src,
				tSp: compressSpArray(textSp), // spaces around texts
				tsr: [ tsr[0], isMeta ? tsr[1] : (tsr[1] - 2) ],
			}),
		];
		if (!isMeta) {
			tokens.push(new EndTagTk(isBlock ? 'div' : 'span', [], {
				tsr: [ tsr[1] - 2, tsr[1] ],
			}));
		}

		return { tokens: tokens };
	}
}

// This is clunky, but we don't have async/await until Node >= 7 (T206035)
LanguageVariantHandler.prototype.onLanguageVariant =
	Promise.async(LanguageVariantHandler.prototype.onLanguageVariantG);

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