// Taken from: https://github.com/walling/unorm/blob/master/lib/unorm.js

/*
	* UnicodeNormalizer 1.0.0
	* Copyright (c) 2008 Matsuza
	* Dual licensed under the MIT (MIT-LICENSE.txt) and
	* GPL (GPL-LICENSE.txt) licenses.
	* $Date: 2008-06-05 16:44:17 +0200 (Thu, 05 Jun 2008) $
	* $Rev: 13309 $
*/

'use strict';

var primitiveSet = require('../../../object/primitive-set')
  , validValue   = require('../../../object/valid-value')
  , data         = require('./_data')

  , floor = Math.floor
  , forms = primitiveSet('NFC', 'NFD', 'NFKC', 'NFKD')

  , DEFAULT_FEATURE = [null, 0, {}], CACHE_THRESHOLD = 10, SBase = 0xAC00
  , LBase = 0x1100, VBase = 0x1161, TBase = 0x11A7, LCount = 19, VCount = 21
  , TCount = 28, NCount = VCount * TCount, SCount = LCount * NCount
  , UChar, cache = {}, cacheCounter = [], i, fromCache, fromData, fromCpOnly
  , fromRuleBasedJamo, fromCpFilter, strategies, UCharIterator
  , RecursDecompIterator, DecompIterator, CompIterator, createIterator
  , normalize;

UChar = function (cp, feature) {
	this.codepoint = cp;
	this.feature = feature;
};

// Strategies
for (i = 0; i <= 0xFF; ++i) cacheCounter[i] = 0;

fromCache = function (next, cp, needFeature) {
	var ret = cache[cp];
	if (!ret) {
		ret = next(cp, needFeature);
		if (!!ret.feature && ++cacheCounter[(cp >> 8) & 0xFF] > CACHE_THRESHOLD) {
			cache[cp] = ret;
		}
	}
	return ret;
};

fromData = function (next, cp, needFeature) {
	var hash = cp & 0xFF00, dunit = UChar.udata[hash] || {}, f = dunit[cp];
	return f ? new UChar(cp, f) : new UChar(cp, DEFAULT_FEATURE);
};
fromCpOnly = function (next, cp, needFeature) {
	return !!needFeature ? next(cp, needFeature) : new UChar(cp, null);
};

fromRuleBasedJamo = function (next, cp, needFeature) {
	var c, base, i, arr, SIndex, TIndex, feature, j;
	if (cp < LBase || (LBase + LCount <= cp && cp < SBase) ||
			(SBase + SCount < cp)) {
		return next(cp, needFeature);
	}
	if (LBase <= cp && cp < LBase + LCount) {
		c = {};
		base = (cp - LBase) * VCount;
		for (i = 0; i < VCount; ++i) {
			c[VBase + i] = SBase + TCount * (i + base);
		}
		arr = new Array(3);
		arr[2] = c;
		return new UChar(cp, arr);
	}

	SIndex = cp - SBase;
	TIndex = SIndex % TCount;
	feature = [];
	if (TIndex !== 0) {
		feature[0] = [SBase + SIndex - TIndex, TBase + TIndex];
	} else {
		feature[0] = [LBase + floor(SIndex / NCount), VBase +
			floor((SIndex % NCount) / TCount)];
		feature[2] = {};
		for (j = 1; j < TCount; ++j) {
			feature[2][TBase + j] = cp + j;
		}
	}
	return new UChar(cp, feature);
};

fromCpFilter = function (next, cp, needFeature) {
	return (cp < 60) || ((13311 < cp) && (cp < 42607))
		? new UChar(cp, DEFAULT_FEATURE) : next(cp, needFeature);
};

strategies = [fromCpFilter, fromCache, fromCpOnly, fromRuleBasedJamo, fromData];

UChar.fromCharCode = strategies.reduceRight(function (next, strategy) {
	return function (cp, needFeature) { return strategy(next, cp, needFeature); };
}, null);

UChar.isHighSurrogate = function (cp) { return cp >= 0xD800 && cp <= 0xDBFF; };
UChar.isLowSurrogate = function (cp) { return cp >= 0xDC00 && cp <= 0xDFFF; };

UChar.prototype.prepFeature = function () {
	if (!this.feature) {
		this.feature = UChar.fromCharCode(this.codepoint, true).feature;
	}
};

UChar.prototype.toString = function () {
	var x;
	if (this.codepoint < 0x10000) return String.fromCharCode(this.codepoint);
	x = this.codepoint - 0x10000;
	return String.fromCharCode(floor(x / 0x400) + 0xD800, x % 0x400 + 0xDC00);
};

UChar.prototype.getDecomp = function () {
	this.prepFeature();
	return this.feature[0] || null;
};

UChar.prototype.isCompatibility = function () {
	this.prepFeature();
	return !!this.feature[1] && (this.feature[1] & (1 << 8));
};
UChar.prototype.isExclude = function () {
	this.prepFeature();
	return !!this.feature[1] && (this.feature[1] & (1 << 9));
};
UChar.prototype.getCanonicalClass = function () {
	this.prepFeature();
	return !!this.feature[1] ? (this.feature[1] & 0xff) : 0;
};
UChar.prototype.getComposite = function (following) {
	var cp;
	this.prepFeature();
	if (!this.feature[2]) return null;
	cp = this.feature[2][following.codepoint];
	return cp ? UChar.fromCharCode(cp) : null;
};

UCharIterator = function (str) {
	this.str = str;
	this.cursor = 0;
};
UCharIterator.prototype.next = function () {
	if (!!this.str && this.cursor < this.str.length) {
		var cp = this.str.charCodeAt(this.cursor++), d;
		if (UChar.isHighSurrogate(cp) && this.cursor < this.str.length &&
				UChar.isLowSurrogate((d = this.str.charCodeAt(this.cursor)))) {
			cp = (cp - 0xD800) * 0x400 + (d - 0xDC00) + 0x10000;
			++this.cursor;
		}
		return UChar.fromCharCode(cp);
	}
	this.str = null;
	return null;
};

RecursDecompIterator = function (it, cano) {
	this.it = it;
	this.canonical = cano;
	this.resBuf = [];
};

RecursDecompIterator.prototype.next = function () {
	var recursiveDecomp, uchar;
	recursiveDecomp = function (cano, uchar) {
		var decomp = uchar.getDecomp(), ret, i, a, j;
		if (!!decomp && !(cano && uchar.isCompatibility())) {
			ret = [];
			for (i = 0; i < decomp.length; ++i) {
				a = recursiveDecomp(cano, UChar.fromCharCode(decomp[i]));
				//ret.concat(a); //<-why does not this work?
				//following block is a workaround.
				for (j = 0; j < a.length; ++j) ret.push(a[j]);
			}
			return ret;
		}
		return [uchar];
	};
	if (this.resBuf.length === 0) {
		uchar = this.it.next();
		if (!uchar) return null;
		this.resBuf = recursiveDecomp(this.canonical, uchar);
	}
	return this.resBuf.shift();
};

DecompIterator = function (it) {
	this.it = it;
	this.resBuf = [];
};

DecompIterator.prototype.next = function () {
	var cc, uchar, inspt, uchar2, cc2;
	if (this.resBuf.length === 0) {
		do {
			uchar = this.it.next();
			if (!uchar) break;
			cc = uchar.getCanonicalClass();
			inspt = this.resBuf.length;
			if (cc !== 0) {
				for (inspt; inspt > 0; --inspt) {
					uchar2 = this.resBuf[inspt - 1];
					cc2 = uchar2.getCanonicalClass();
					if (cc2 <= cc) break;
				}
			}
			this.resBuf.splice(inspt, 0, uchar);
		} while (cc !== 0);
	}
	return this.resBuf.shift();
};

CompIterator = function (it) {
	this.it = it;
	this.procBuf = [];
	this.resBuf = [];
	this.lastClass = null;
};

CompIterator.prototype.next = function () {
	var uchar, starter, composite, cc;
	while (this.resBuf.length === 0) {
		uchar = this.it.next();
		if (!uchar) {
			this.resBuf = this.procBuf;
			this.procBuf = [];
			break;
		}
		if (this.procBuf.length === 0) {
			this.lastClass = uchar.getCanonicalClass();
			this.procBuf.push(uchar);
		} else {
			starter = this.procBuf[0];
			composite = starter.getComposite(uchar);
			cc = uchar.getCanonicalClass();
			if (!!composite && (this.lastClass < cc || this.lastClass === 0)) {
				this.procBuf[0] = composite;
			} else {
				if (cc === 0) {
					this.resBuf = this.procBuf;
					this.procBuf = [];
				}
				this.lastClass = cc;
				this.procBuf.push(uchar);
			}
		}
	}
	return this.resBuf.shift();
};

createIterator = function (mode, str) {
	switch (mode) {
	case "NFD":
		return new DecompIterator(
			new RecursDecompIterator(new UCharIterator(str), true)
		);
	case "NFKD":
		return new DecompIterator(
			new RecursDecompIterator(new UCharIterator(str), false)
		);
	case "NFC":
		return new CompIterator(new DecompIterator(
			new RecursDecompIterator(new UCharIterator(str), true)
		));
	case "NFKC":
		return new CompIterator(new DecompIterator(
			new RecursDecompIterator(new UCharIterator(str), false)
		));
	}
	throw mode + " is invalid";
};
normalize = function (mode, str) {
	var it = createIterator(mode, str), ret = "", uchar;
	while (!!(uchar = it.next())) ret += uchar.toString();
	return ret;
};

/* Unicode data */
UChar.udata =  data;

module.exports = function (/*form*/) {
	var str = String(validValue(this)), form = arguments[0];
	if (form === undefined) form = 'NFC';
	else form = String(form);
	if (!forms[form]) throw new RangeError('Invalid normalization form: ' + form);
	return normalize(form, str);
};
