/**
 * This is a demonstration of content model handling in extensions for
 * Parsoid.  It implements the "json" content model, to allow editing
 * JSON data structures using Visual Editor.  It represents the JSON
 * structure as a nested table.
 * @module ext/JSON
 */

'use strict';

const ParsoidExtApi = module.parent.require('./extapi.js').versionCheck('^0.10.0');
const {
	DOMDataUtils,
	DOMUtils,
	Promise,
	addMetaData,
} = ParsoidExtApi;

/**
 * Native Parsoid implementation of the "json" contentmodel.
 * @class
 */
var JSONExt = function() {
	/** @type {Object} */
	this.config = {
		contentmodels: {
			json: this,
		},
	};
};

var PARSE_ERROR_HTML =
	'<!DOCTYPE html><html>' +
	'<body>' +
	'<table data-mw=\'{"errors":[{"key":"bad-json"}]}\' typeof="mw:Error">' +
	'</body>';

/**
 * JSON to HTML.
 * Implementation matches that from includes/content/JsonContent.php in
 * mediawiki core, except that we add some additional classes to distinguish
 * value types.
 * @param {MWParserEnvironment} env
 * @return {Document}
 * @method
 */
JSONExt.prototype.toHTML = Promise.method(function(env) {
	var document = env.createDocument('<!DOCTYPE html><html><body>');
	var rootValueTable;
	var objectTable;
	var objectRow;
	var arrayTable;
	var valueCell;
	var primitiveValue;
	var src;

	rootValueTable = function(parent, val) {
		if (Array.isArray(val)) {
			// Wrap arrays in another array so they're visually boxed in a
			// container.  Otherwise they are visually indistinguishable from
			// a single value.
			return arrayTable(parent, [ val ]);
		}
		if (val && typeof val === "object") {
			return objectTable(parent, val);
		}
		parent.innerHTML =
			'<table class="mw-json mw-json-single-value"><tbody><tr><td>';
		return primitiveValue(parent.querySelector('td'), val);
	};
	objectTable = function(parent, val) {
		parent.innerHTML = '<table class="mw-json mw-json-object"><tbody>';
		var tbody = parent.firstElementChild.firstElementChild;
		var keys = Object.keys(val);
		if (keys.length) {
			keys.forEach(function(k) {
				objectRow(tbody, k, val[k]);
			});
		} else {
			tbody.innerHTML =
				'<tr><td class="mw-json-empty">';
		}
	};
	objectRow = function(parent, key, val) {
		var tr = document.createElement('tr');
		if (key !== undefined) {
			var th = document.createElement('th');
			th.textContent = key;
			tr.appendChild(th);
		}
		valueCell(tr, val);
		parent.appendChild(tr);
	};
	arrayTable = function(parent, val) {
		parent.innerHTML = '<table class="mw-json mw-json-array"><tbody>';
		var tbody = parent.firstElementChild.firstElementChild;
		if (val.length) {
			for (var i = 0; i < val.length; i++) {
				objectRow(tbody, undefined, val[i]);
			}
		} else {
			tbody.innerHTML =
				'<tr><td class="mw-json-empty">';
		}
	};
	valueCell = function(parent, val) {
		var td = document.createElement('td');
		if (Array.isArray(val)) {
			arrayTable(td, val);
		} else if (val && typeof val === 'object') {
			objectTable(td, val);
		} else {
			td.classList.add('value');
			primitiveValue(td, val);
		}
		parent.appendChild(td);
	};
	primitiveValue = function(parent, val) {
		if (val === null) {
			parent.classList.add('mw-json-null');
		} else if (val === true || val === false) {
			parent.classList.add('mw-json-boolean');
		} else if (typeof val === 'number') {
			parent.classList.add('mw-json-number');
		} else if (typeof val === 'string') {
			parent.classList.add('mw-json-string');
		}
		parent.textContent = '' + val;
	};

	try {
		src = JSON.parse(env.page.src);
		rootValueTable(document.body, src);
	} catch (e) {
		document = env.createDocument(PARSE_ERROR_HTML);
	}
	// We're responsible for running the standard DOMPostProcessor on our
	// resulting document.
	if (env.pageBundle) {
		DOMDataUtils.visitAndStoreDataAttribs(document.body, {
			storeInPageBundle: env.pageBundle,
			env: env,
		});
	}
	addMetaData(env, document);
	return document;
});

/**
 * HTML to JSON.
 * @param {MWParserEnvironment} env
 * @param {Node} body
 * @param {boolean} useSelser
 * @return {string}
 * @method
 */
JSONExt.prototype.fromHTML = Promise.method(function(env, body, useSelser) {
	var rootValueTable;
	var objectTable;
	var objectRow;
	var arrayTable;
	var valueCell;
	var primitiveValue;

	console.assert(DOMUtils.isBody(body), 'Expected a body node.');

	rootValueTable = function(el) {
		if (el.classList.contains('mw-json-single-value')) {
			return primitiveValue(el.querySelector('tr > td'));
		} else if (el.classList.contains('mw-json-array')) {
			return arrayTable(el)[0];
		} else {
			return objectTable(el);
		}
	};
	objectTable = function(el) {
		console.assert(el.classList.contains('mw-json-object'));
		var tbody = el;
		if (
			tbody.firstElementChild &&
			tbody.firstElementChild.tagName === 'TBODY'
		) {
			tbody = tbody.firstElementChild;
		}
		var rows = tbody.children;
		var obj = {};
		var empty = rows.length === 0 || (
			rows[0].firstElementChild &&
			rows[0].firstElementChild.classList.contains('mw-json-empty')
		);
		if (!empty) {
			for (var i = 0; i < rows.length; i++) {
				objectRow(rows[i], obj, undefined);
			}
		}
		return obj;
	};
	objectRow = function(tr, obj, key) {
		var td = tr.firstElementChild;
		if (key === undefined) {
			key = td.textContent;
			td = td.nextElementSibling;
		}
		obj[key] = valueCell(td);
	};
	arrayTable = function(el) {
		console.assert(el.classList.contains('mw-json-array'));
		var tbody = el;
		if (
			tbody.firstElementChild &&
			tbody.firstElementChild.tagName === 'TBODY'
		) {
			tbody = tbody.firstElementChild;
		}
		var rows = tbody.children;
		var arr = [];
		var empty = rows.length === 0 || (
			rows[0].firstElementChild &&
			rows[0].firstElementChild.classList.contains('mw-json-empty')
		);
		if (!empty) {
			for (var i = 0; i < rows.length; i++) {
				objectRow(rows[i], arr, i);
			}
		}
		return arr;
	};
	valueCell = function(el) {
		console.assert(el.tagName === 'TD');
		var table = el.firstElementChild;
		if (table && table.classList.contains('mw-json-array')) {
			return arrayTable(table);
		} else if (table && table.classList.contains('mw-json-object')) {
			return objectTable(table);
		} else {
			return primitiveValue(el);
		}
	};
	primitiveValue = function(el) {
		if (el.classList.contains('mw-json-null')) {
			return null;
		} else if (el.classList.contains('mw-json-boolean')) {
			return /true/.test(el.textContent);
		} else if (el.classList.contains('mw-json-number')) {
			return +el.textContent;
		} else if (el.classList.contains('mw-json-string')) {
			return '' + el.textContent;
		} else {
			return undefined; // shouldn't happen.
		}
	};
	var t = body.firstElementChild;
	console.assert(t && t.tagName === 'TABLE');
	return JSON.stringify(rootValueTable(t), null, 4);
});

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