/** @module */

'use strict';

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

var ParserEnv = require('./config/MWParserEnvironment.js').MWParserEnvironment;
var LanguageConverter = require('./language/LanguageConverter').LanguageConverter;
var AddRedLinks = require('./wt2html/pp/processors/AddRedLinks').AddRedLinks;
var ParsoidConfig = require('./config/ParsoidConfig.js').ParsoidConfig;
var TemplateRequest = require('./mw/ApiRequest.js').TemplateRequest;
var ContentUtils = require('./utils/ContentUtils.js').ContentUtils;
var DOMDataUtils = require('./utils/DOMDataUtils.js').DOMDataUtils;
var DOMUtils = require('./utils/DOMUtils.js').DOMUtils;
var Promise = require('./utils/promise.js');
var JSUtils = require('./utils/jsutils.js').JSUtils;

var _toHTML, _fromHTML;

/**
 * Transform content-model to html
 * (common-case will be wikitext -> html)
 *
 * @param {Object} obj See below
 * @param {MWParserEnvironment} env
 * @param {string} str
 *
 * @return {Promise} Assuming we're ending at html
 *   @return {string} return.html
 *   @return {Array} return.lint The lint buffer
 *   @return {string} return.contentmodel
 *   @return {Object} return.headers HTTP language-related headers
 *   @return {string} return.headers.content-language Page language or variant
 *   @return {string} return.headers.vary Indicates whether variant conversion
 *     was done or could be done
 *   @return {Object} [return.pb] If pageBundle was requested
 */
_toHTML = Promise.async(function *(obj, env, str) {
	// `str` will be `undefined` when we fetched page source and info,
	// which we don't want to overwrite.
	if (str !== undefined) {
		env.setPageSrcInfo(str);
	}
	var handler = env.getContentHandler(obj.contentmodel);
	var doc = yield handler.toHTML(env);
	var out;
	if (env.pageBundle) {
		out = ContentUtils.extractDpAndSerialize(obj.body_only ? doc.body : doc, {
			innerXML: obj.body_only,
		});
	} else {
		out = {
			html: ContentUtils.toXML(obj.body_only ? doc.body : doc, {
				innerXML: obj.body_only,
			}),
		};
	}

	if (env.conf.parsoid.linting) {
		out.lint = env.lintLogger.buffer;
		yield env.log("end/parse"); // wait for linter logging to complete
	}
	out.contentmodel = (obj.contentmodel || env.page.getContentModel());
	out.headers = DOMUtils.findHttpEquivHeaders(doc);
	return out;
});

/**
 * Transform html to requested content-model
 *
 * @param {Object} obj See below
 * @param {MWParserEnvironment} env
 * @param {string} html
 * @param {Object} pb
 *
 * @return {Promise} Assuming we're ending at wt
 *   @return {string} return.wt
 */
_fromHTML = Promise.async(function *(obj, env, html, pb) {
	var useSelser = (obj.selser !== undefined);
	var doc = env.createDocument(html);
	pb = pb || DOMDataUtils.extractPageBundle(doc);
	if (useSelser && env.page.dom) {
		pb = pb || DOMDataUtils.extractPageBundle(env.page.dom.ownerDocument);
		if (pb) {
			DOMDataUtils.applyPageBundle(env.page.dom.ownerDocument, pb);
		}
	}
	if (pb) {
		DOMDataUtils.applyPageBundle(doc, pb);
	}
	var handler = env.getContentHandler(obj.contentmodel);
	var out = yield handler.fromHTML(env, doc.body, useSelser);
	return { wt: out };
});

/**
 * @param {Object} obj See below
 * @param {MWParserEnvironment} env
 * @param {string} html
 */
const _languageConversion = function(obj, env, html) {
	const doc = env.createDocument(html);
	// Note that `maybeConvert` could still be a no-op, in case the
	// __NOCONTENTCONVERT__ magic word is present, or the targetVariant
	// is a base language code or otherwise invalid.
	LanguageConverter.maybeConvert(
		env, doc, obj.variant.target,
		// FIXME: Setting this is untested and broken!
		null  // obj.variant.source
	);
	// Ensure there's a <head>
	if (!doc.head) {
		doc.documentElement
			.insertBefore(doc.createElement('head'), doc.body);
	}
	// Update content-language and vary headers.
	const ensureHeader = (h) => {
		let el = doc.querySelector(`meta[http-equiv="${h}"i]`);
		if (!el) {
			el = doc.createElement('meta');
			el.setAttribute('http-equiv', h);
			doc.head.appendChild(el);
		}
		return el;
	};
	ensureHeader('content-language')
		.setAttribute('content', env.htmlContentLanguage());
	ensureHeader('vary')
		.setAttribute('content', env.htmlVary());
	// Serialize & emit.
	return {
		html: ContentUtils.toXML(obj.body_only ? doc.body : doc, {
			innerXML: obj.body_only,
		}),
		headers: DOMUtils.findHttpEquivHeaders(doc),
	};
};

const _updateRedLinks = Promise.async(function *(obj, env, html) {
	var doc = env.createDocument(html);
	// Note: this only works if the configured wiki has the ParsoidBatchAPI
	// extension installed.
	yield AddRedLinks.addRedLinks(env, doc);
	// No need to `ContentUtils.extractDpAndSerialize`, it wasn't applied.
	return {
		html: ContentUtils.toXML(obj.body_only ? doc.body : doc, {
			innerXML: obj.body_only,
		}),
		headers: DOMUtils.findHttpEquivHeaders(doc),
	};
});

/**
 * Map of JSON.stringified parsoidOptions to ParsoidConfig
 */
var configCache = new Map();

/**
 * Parse wikitext (or html) to html (or wikitext).
 *
 * @param {Object} obj
 * @param {string} obj.input The string to parse
 * @param {string} obj.mode The mode to use
 * @param {Object} obj.parsoidOptions Will be Object.assign'ed to ParsoidConfig
 * @param {Object} obj.envOptions Will be Object.assign'ed to the env
 * @param {boolean} [obj.cacheConfig] Cache the constructed ParsoidConfig
 * @param {boolean} [obj.body_only] Only return the <body> children (T181657)
 * @param {Number} [obj.oldid]
 * @param {Object} [obj.selser]
 * @param {Object} [obj.pb]
 * @param {string} [obj.contentmodel]
 * @param {string} [obj.outputContentVersion]
 * @param {Object} [obj.reuseExpansions]
 * @param {string} [obj.pagelanguage]
 * @param {Object} [obj.variant]
 * @param {Function} [cb] Optional node-style callback
 *
 * @return {Promise}
 */
module.exports = Promise.async(function *(obj) {
	var start = JSUtils.startTime();

	// Enforce the contraints of passing to a worker
	obj = JSON.parse(JSON.stringify(obj));

	var hash = JSON.stringify(obj.parsoidOptions);
	var parsoidConfig;
	if (obj.cacheConfig && configCache.has(hash)) {
		parsoidConfig = configCache.get(hash);
	} else {
		parsoidConfig = new ParsoidConfig(null, obj.parsoidOptions);
		if (obj.cacheConfig) {
			configCache.set(hash, parsoidConfig);
			// At present, we don't envision using the cache with multiple
			// configurations.  Prevent it from growing unbounded inadvertently.
			console.assert(configCache.size === 1, 'Config properties changed.');
		}
	}

	var env = yield ParserEnv.getParserEnv(parsoidConfig, obj.envOptions);
	env.startTime = start;
	var s1 = JSUtils.startTime();
	env.bumpTimeUse("Setup Environment", s1 - start, 'Init');
	env.log('info', 'started ' + obj.mode);
	try {

		if (obj.oldid) {
			env.page.meta.revision.revid = obj.oldid;
		}

		var out;
		if (obj.mode === 'variant') {
			env.page.pagelanguage = obj.pagelanguage;
			return _languageConversion(obj, env, obj.input);
		} else if (obj.mode === 'redlinks') {
			return _updateRedLinks(obj, env, obj.input);
		} else if (['html2wt', 'html2html', 'selser'].includes(obj.mode)) {
			// Selser
			var selser = obj.selser;
			if (selser !== undefined) {
				if (selser.oldtext !== null) {
					env.setPageSrcInfo(selser.oldtext);
				}
				if (selser.oldhtml) {
					env.page.dom = env.createDocument(selser.oldhtml).body;
				}
				if (selser.domdiff) {
					// FIXME: need to load diff markers from attributes
					env.page.domdiff = {
						isEmpty: false,
						dom: ContentUtils.ppToDOM(env, selser.domdiff),
					};
					throw new Error('this is broken');
				}
			}
			var html = obj.input;
			env.bumpHtml2WtResourceUse('htmlSize', html.length);
			out = yield _fromHTML(obj, env, html, obj.pb);
			return obj.mode === 'html2html' ? _toHTML(obj, env, out.wt) : out;
		} else { /* wt2html, wt2wt */
			// The content version to output
			if (obj.outputContentVersion) {
				env.setOutputContentVersion(obj.outputContentVersion);
			}

			if (obj.reuseExpansions) {
				env.cacheReusableExpansions(obj.reuseExpansions);
			}

			var wt = obj.input;

			// Always fetch page info if we have an oldid
			if (obj.oldid || wt === undefined) {
				var target = env.normalizeAndResolvePageTitle();
				yield TemplateRequest.setPageSrcInfo(env, target, obj.oldid);
				env.bumpTimeUse("Pre-parse (source fetch)", JSUtils.elapsedTime(s1), 'Init');
				// Ensure that we don't env.page.reset() when calling
				// env.setPageSrcInfo(wt) in _toHTML()
				if (wt !== undefined) {
					env.topFrame.srcText = env.page.src = wt;
					wt = undefined;
				}
			}

			var wikitextSize = wt !== undefined ? wt.length : env.page.src.length;
			env.bumpWt2HtmlResourceUse('wikitextSize', wikitextSize);
			if (parsoidConfig.metrics) {
				var mstr = obj.envOptions.pageWithOldid ? 'pageWithOldid' : 'wt';
				parsoidConfig.metrics.timing(`wt2html.${mstr}.size.input`, wikitextSize);
			}

			// Explicitly setting the pagelanguage can override the fetched one
			if (obj.pagelanguage) {
				env.page.pagelanguage = obj.pagelanguage;
			}

			out = yield _toHTML(obj, env, wt);
			return obj.mode === 'wt2html' ? out : _fromHTML(obj, env, out.html);
		}
	} finally {
		var end = JSUtils.elapsedTime(start);
		yield env.log('info', `completed ${obj.mode} in ${end}ms`);
	}
}, 1);