'use strict';

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var streamReaderUtils = require('@emmetio/stream-reader-utils');
var Node = _interopDefault(require('@emmetio/node'));
var StreamReader = _interopDefault(require('@emmetio/stream-reader'));

/**
 * A wrapper for holding CSS value
 */
class CSSValue {
	constructor() {
		this.type = 'css-value';
		this.value = [];
	}

	get size() {
		return this.value.length;
	}

	add(value) {
		this.value.push(value);
	}

	has(value) {
		return this.value.indexOf(value) !== -1;
	}

	toString() {
		return this.value.join(' ');
	}
}

const HASH = 35; // #
const DOT  = 46; // .

/**
 * Consumes a color token from given string
 * @param  {StreamReader} stream
 * @return {Color} Returns consumend color object, `undefined` otherwise
 */
function consumeColor(stream) {
	// supported color variations:
	// #abc   → #aabbccc
	// #0     → #000000
	// #fff.5 → rgba(255, 255, 255, 0.5)
	// #t     → transparent
	if (stream.peek() === HASH) {
		stream.start = stream.pos;
		stream.next();

		stream.eat(116) /* t */ || stream.eatWhile(isHex);
		const base = stream.current();

		// a hex color can be followed by `.num` alpha value
		stream.start = stream.pos;
		if (stream.eat(DOT) && !stream.eatWhile(streamReaderUtils.isNumber)) {
			throw stream.error('Unexpected character for alpha value of color');
		}

		return new Color(base, stream.current());
	}
}

class Color {
	constructor(value, alpha) {
		this.type = 'color';
		this.raw = value;
		this.alpha = Number(alpha != null && alpha !== '' ? alpha : 1);
		value = value.slice(1); // remove #

		let r = 0, g = 0, b = 0;

		if (value === 't') {
			this.alpha = 0;
		} else {
			switch (value.length) {
				case 0:
					break;

				case 1:
					r = g = b = value + value;
					break;

				case 2:
					r = g = b = value;
					break;

				case 3:
					r = value[0] + value[0];
					g = value[1] + value[1];
					b = value[2] + value[2];
					break;

				default:
					value += value;
					r = value.slice(0, 2);
					g = value.slice(2, 4);
					b = value.slice(4, 6);
			}
		}

		this.r = parseInt(r, 16);
		this.g = parseInt(g, 16);
		this.b = parseInt(b, 16);
	}

	/**
	 * Output current color as hex value
	 * @param {Boolean} shor Produce short value (e.g. #fff instead of #ffffff), if possible
	 * @return {String}
	 */
	toHex(short) {
		const fn = (short && isShortHex(this.r) && isShortHex(this.g) && isShortHex(this.b))
			? toShortHex : toHex;

		return '#' + fn(this.r)  + fn(this.g) + fn(this.b);
	}

	/**
	 * Output current color as `rgba?(...)` CSS color
	 * @return {String}
	 */
	toRGB() {
		const values = [this.r, this.g, this.b];
		if (this.alpha !== 1) {
			values.push(this.alpha.toFixed(8).replace(/\.?0+$/, ''));
		}

		return `${values.length === 3 ? 'rgb' : 'rgba'}(${values.join(', ')})`;
	}

	toString(short) {
		if (!this.r && !this.g && !this.b && !this.alpha) {
			return 'transparent';
		}
		return this.alpha === 1 ? this.toHex(short) : this.toRGB();
	}
}

/**
 * Check if given code is a hex value (/0-9a-f/)
 * @param  {Number}  code
 * @return {Boolean}
 */
function isHex(code) {
	return streamReaderUtils.isNumber(code) || streamReaderUtils.isAlpha(code, 65, 70); // A-F
}

function isShortHex(hex) {
	return !(hex % 17);
}

function toShortHex(num) {
	return (num >> 4).toString(16);
}

function toHex(num) {
	return pad(num.toString(16), 2);
}

function pad(value, len) {
	while (value.length < len) {
		value = '0' + value;
	}
	return value;
}

/**
 * @param  {Number}  code
 * @return {Boolean}
 */
function isAlphaNumericWord(code) {
	return streamReaderUtils.isNumber(code) || isAlphaWord(code);
}

/**
 * @param  {Number}  code
 * @return {Boolean}
 */
function isAlphaWord(code) {
	return code === 95 /* _ */ || streamReaderUtils.isAlpha(code);
}

const PERCENT = 37; // %
const DOT$1     = 46; // .
const DASH    = 45; // -

/**
 * Consumes numeric CSS value (number with optional unit) from current stream,
 * if possible
 * @param  {StreamReader} stream
 * @return {NumericValue}
 */
function consumeNumericValue(stream) {
	stream.start = stream.pos;
	if (eatNumber(stream)) {
		const num = stream.current();
		stream.start = stream.pos;

		// eat unit, which can be a % or alpha word
		stream.eat(PERCENT) || stream.eatWhile(isAlphaWord);
		return new NumericValue(num, stream.current());
	}
}

/**
 * A numeric CSS value with optional unit
 */
class NumericValue {
	constructor(value, unit) {
		this.type = 'numeric';
		this.value = Number(value);
		this.unit = unit || '';
	}

	toString() {
		return `${this.value}${this.unit}`;
	}
}

/**
 * Eats number value from given stream
 * @param  {StreamReader} stream
 * @return {Boolean} Returns `true` if number was consumed
 */
function eatNumber(stream) {
	const start = stream.pos;
	const negative = stream.eat(DASH);
	let hadDot = false, consumed = false, code;

	while (!stream.eof()) {
		code = stream.peek();

		// either a second dot or not a number: stop parsing
		if (code === DOT$1 ? hadDot : !streamReaderUtils.isNumber(code)) {
			break;
		}

		consumed = true;

		if (code === DOT$1) {
			hadDot = true;
		}

		stream.next();
	}

	if (negative && !consumed) {
		// edge case: consumed dash only, bail out
		stream.pos = start;
	}

	return start !== stream.pos;
}

const DOLLAR = 36; // $
const DASH$1   = 45; // -
const AT     = 64; // @

/**
 * Consumes a keyword: either a variable (a word that starts with $ or @) or CSS
 * keyword or shorthand
 * @param  {StreamReader} stream
 * @param  {Boolean} [short] Use short notation for consuming value.
 * The difference between “short” and “full” notation is that first one uses
 * alpha characters only and used for extracting keywords from abbreviation,
 * while “full” notation also supports numbers and dashes
 * @return {String} Consumed variable
 */
function consumeKeyword(stream, short) {
	stream.start = stream.pos;

	if (stream.eat(DOLLAR) || stream.eat(AT)) {
		// SCSS or LESS variable
		stream.eatWhile(isVariableName);
	} else if (short) {
		stream.eatWhile(isAlphaWord);
	} else {
		stream.eatWhile(isKeyword);
	}

	return stream.start !== stream.pos ? new Keyword(stream.current()) : null;
}

class Keyword {
	constructor(value) {
		this.type = 'keyword';
		this.value = value;
	}

	toString() {
		return this.value;
	}
}

function isKeyword(code) {
	return isAlphaNumericWord(code) || code === DASH$1;
}

function isVariableName(code) {
	return code === 45 /* - */ || isAlphaNumericWord(code);
}

const opt = { throws: true };

/**
 * Consumes 'single' or "double"-quoted string from given string, if possible
 * @param  {StreamReader} stream
 * @return {String}
 */
function consumeQuoted(stream) {
	if (streamReaderUtils.eatQuoted(stream, opt)) {
		return new QuotedString(stream.current());
	}
}

class QuotedString {
	constructor(value) {
		this.type = 'string';
		this.value = value;
	}

	toString() {
		return this.value;
	}
}

const LBRACE = 40; // (
const RBRACE = 41; // )
const COMMA  = 44; // ,

/**
 * Consumes arguments from given string.
 * Arguments are comma-separated list of CSS values inside round braces, e.g.
 * `(1, a2, 'a3')`. Nested lists and quoted strings are supported
 * @param  {StreamReader} stream
 * @return {Array}        Array of arguments, `null` if arguments cannot be consumed
 */
function consumeArgumentList(stream) {
	if (!stream.eat(LBRACE)) {
		// not an argument list
		return null;
	}

	let arg;
	const argsList = [];

	while (!stream.eof()) {
		if (arg = consumeArgument(stream)) {
			argsList.push(arg);
		} else {
			// didn’t consumed argument, expect argument separator or end-of-arguments
			stream.eatWhile(streamReaderUtils.isWhiteSpace);

			if (stream.eat(RBRACE)) {
				// end of arguments list
				break;
			}

			if (!stream.eat(COMMA)) {
				throw stream.error('Expected , or )');
			}
		}
	}

	return argsList;
}

/**
 * Consumes a single argument. An argument is a `CSSValue`, e.g. it could be
 * a space-separated string of value
 * @param  {StreamReader} stream
 * @return {CSSValue}
 */
function consumeArgument(stream) {
	const result = new CSSValue();
	let value;

	while (!stream.eof()) {
		stream.eatWhile(streamReaderUtils.isWhiteSpace);
		value = consumeNumericValue(stream) || consumeColor(stream)
			|| consumeQuoted(stream) || consumeKeywordOrFunction(stream);

		if (!value) {
			break;
		}

		result.add(value);
	}

	return result.size ? result : null;
}

/**
 * Consumes either function call like `foo()` or keyword like `foo`
 * @param  {StreamReader} stream
 * @return {Keyword|FunctionCall}
 */
function consumeKeywordOrFunction(stream) {
	const kw = consumeKeyword(stream);
	if (kw) {
		const args = consumeArgumentList(stream);
		return args ? new FunctionCall(kw.toString(), args) : kw;
	}
}

class FunctionCall {
	/**
	 * @param {String} name Function name
	 * @param {Array}  args Function arguments
	 */
	constructor(name, args) {
		this.type = 'function';
		this.name = name;
		this.args = args || [];
	}

	toString() {
		return `${this.name}(${this.args.join(', ')})`;
	}
}

const EXCL   = 33; // !
const DOLLAR$1 = 36; // $
const PLUS   = 43; // +
const DASH$2   = 45; // -
const COLON  = 58; // :
const AT$1     = 64; // @

/**
 * Parses given Emmet CSS abbreviation and returns it as parsed Node tree
 * @param {String} abbr
 * @return {Node}
 */
function index(abbr) {
	const root = new Node();
	const stream = new StreamReader(abbr);

	while (!stream.eof()) {
		let node = new Node(consumeIdent(stream));
		node.value = consumeValue(stream);

		const args = consumeArgumentList(stream);
		if (args) {
			// technically, arguments in CSS are anonymous Emmet Node attributes,
			// but since Emmet can support only one anonymous, `null`-name
			// attribute (for good reasons), we’ll use argument index as name
			for (let i = 0; i < args.length; i++) {
				node.setAttribute(String(i), args[i]);
			}
		}

		// Consume `!important` modifier at the end of expression
		if (stream.eat(EXCL)) {
			node.value.add('!');
		}

		root.appendChild(node);

		// CSS abbreviations cannot be nested, only listed
		if (!stream.eat(PLUS)) {
			break;
		}
	}

	if (!stream.eof()) {
		throw stream.error('Unexpected character');
	}

	return root;
}

/**
 * Consumes CSS property identifier from given stream
 * @param  {StreamReader} stream
 * @return {String}
 */
function consumeIdent(stream) {
	stream.start = stream.pos;
	stream.eatWhile(isIdentPrefix);
	stream.eatWhile(isIdent);
	return stream.start !== stream.pos ? stream.current() : null;
}

/**
 * Consumes embedded value from Emmet CSS abbreviation stream
 * @param  {StreamReader} stream
 * @return {CSSValue}
 */
function consumeValue(stream) {
	const values = new CSSValue();
	let value;

	while (!stream.eof()) {
		// use colon as value separator
		stream.eat(COLON);
		if (value = consumeNumericValue(stream) || consumeColor(stream)) {
			// edge case: a dash after unit-less numeric value or color should
			// be treated as value separator, not negative sign
			if (!value.unit) {
				stream.eat(DASH$2);
			}
		} else {
			stream.eat(DASH$2);
			value = consumeKeyword(stream, true);
		}

		if (!value) {
			break;
		}

		values.add(value);
	}

	return values;
}

/**
 * @param  {Number}  code
 * @return {Boolean}
 */
function isIdent(code) {
	return isAlphaWord(code);
}

/**
 * @param  {Number}  code
 * @return {Boolean}
 */
function isIdentPrefix(code) {
	return code === AT$1 || code === DOLLAR$1 || code === EXCL;
}

module.exports = index;
