/** @module tokens/Token */

'use strict';

const KV = require('./KV.js').KV;

/**
 * Catch-all class for all token types.
 * @abstract
 * @class
 */
class Token {
	/**
	 * Generic set attribute method.
	 *
	 * @param {string} name
	 * @param {any} value
	 */
	addAttribute(name, value, srcOffsets) {
		this.attribs.push(new KV(name, value, srcOffsets));
	}

	/**
	 * Generic set attribute method with support for change detection.
	 * Set a value and preserve the original wikitext that produced it.
	 *
	 * @param {string} name
	 * @param {any} value
	 * @param {any} origValue
	 */
	addNormalizedAttribute(name, value, origValue) {
		this.addAttribute(name, value);
		this.setShadowInfo(name, value, origValue);
	}

	/**
	 * Generic attribute accessor.
	 *
	 * @param {string} name
	 * @return {any}
	 */
	getAttribute(name) {
		return KV.lookup(this.attribs, name);
	}

	getAttributeKV(name) {
		return KV.lookupKV(this.attribs, name);
	}

	/**
	 * Generic attribute accessor.
	 *
	 * @param {string} name
	 * @return {boolean}
	 */
	hasAttribute(name) {
		return this.getAttributeKV(name) !== null;
	}

	/**
	 * Set an unshadowed attribute.
	 *
	 * @param {string} name
	 * @param {any} value
	 */
	setAttribute(name, value) {
		// First look for the attribute and change the last match if found.
		for (var i = this.attribs.length - 1; i >= 0; i--) {
			var kv = this.attribs[i];
			var k = kv.k;
			if (k.constructor === String && k.toLowerCase() === name) {
				kv.v = value;
				this.attribs[i] = kv;
				return;
			}
		}
		// Nothing found, just add the attribute
		this.addAttribute(name, value);
	}

	/**
	 * Store the original value of an attribute in a token's dataAttribs.
	 *
	 * @param {string} name
	 * @param {any} value
	 * @param {any} origValue
	 */
	setShadowInfo(name, value, origValue) {
		// Don't shadow if value is the same or the orig is null
		if (value !== origValue && origValue !== null) {
			if (!this.dataAttribs.a) {
				this.dataAttribs.a = {};
			}
			this.dataAttribs.a[name] = value;
			if (!this.dataAttribs.sa) {
				this.dataAttribs.sa = {};
			}
			if (origValue !== undefined) {
				this.dataAttribs.sa[name] = origValue;
			}
		}
	}

	/**
	 * Attribute info accessor for the wikitext serializer. Performs change
	 * detection and uses unnormalized attribute values if set. Expects the
	 * context to be set to a token.
	 *
	 * @param {string} name
	 * @return {Object} Information about the shadow info attached to this attribute.
	 * @return {any} return.value
	 * @return {boolean} return.modified Whether the attribute was changed between parsing and now.
	 * @return {boolean} return.fromsrc Whether we needed to get the source of the attribute to round-trip it.
	 */
	getAttributeShadowInfo(name) {
		var curVal = this.getAttribute(name);

		// Not the case, continue regular round-trip information.
		if (this.dataAttribs.a === undefined ||
				this.dataAttribs.a[name] === undefined) {
			return {
				value: curVal,
				// Mark as modified if a new element
				modified: Object.keys(this.dataAttribs).length === 0,
				fromsrc: false,
			};
		} else if (this.dataAttribs.a[name] !== curVal) {
			return {
				value: curVal,
				modified: true,
				fromsrc: false,
			};
		} else if (this.dataAttribs.sa === undefined ||
				this.dataAttribs.sa[name] === undefined) {
			return {
				value: curVal,
				modified: false,
				fromsrc: false,
			};
		} else {
			return {
				value: this.dataAttribs.sa[name],
				modified: false,
				fromsrc: true,
			};
		}
	}

	/**
	 * Completely remove all attributes with this name.
	 *
	 * @param {string} name
	 */
	removeAttribute(name) {
		var out = [];
		var attribs = this.attribs;
		for (var i = 0, l = attribs.length; i < l; i++) {
			var kv = attribs[i];
			if (kv.k.toLowerCase() !== name) {
				out.push(kv);
			}
		}
		this.attribs = out;
	}

	/**
	 * Add a space-separated property value.
	 *
	 * @param {string} name
	 * @param {any} value The value to add to the attribute.
	 */
	addSpaceSeparatedAttribute(name, value) {
		var curVal = this.getAttributeKV(name);
		var vals;
		if (curVal !== null) {
			vals = curVal.v.split(/\s+/);
			for (var i = 0, l = vals.length; i < l; i++) {
				if (vals[i] === value) {
					// value is already included, nothing to do.
					return;
				}
			}
			// Value was not yet included in the existing attribute, just add
			// it separated with a space
			this.setAttribute(curVal.k, curVal.v + ' ' + value);
		} else {
			// the attribute did not exist at all, just add it
			this.addAttribute(name, value);
		}
	}

	/**
	 * Get the wikitext source of a token.
	 *
	 * @param {Frame|null} frame
	 * @return {string}
	 */
	getWTSource(frame) {
		const tsr = this.dataAttribs.tsr;
		console.assert(Array.isArray(tsr), 'Expected token to have tsr info.');
		const srcText = frame.srcText;
		return srcText.substring(tsr[0], tsr[1]);
	}
}

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