var Base64 = {
	// private property
	_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",

	// public method for encoding
	encode: function(string) {
		return this._encodeArray(this._utf8_encode(string));
	},

	// public method for decoding
	decode: function(string) {
		return this._utf8_decode(this._decodeArray(string));
	},

	_encodeArray: function(u) {
		var i = 0;
		var a = [];
		var n = 0 | (u.length / 3);
		while(0 < n--) {
				var v = (u[i] << 16) + (u[i+1] << 8) + u[i+2];
				i += 3;
				a.push(this._keyStr.charAt(63 & (v >> 18)));
				a.push(this._keyStr.charAt(63 & (v >> 12)));
				a.push(this._keyStr.charAt(63 & (v >> 6)));
				a.push(this._keyStr.charAt(63 & v));
		}
		if((u.length - i) == 2) {
				var v = (u[i] << 16) + (u[i+1] << 8);
				a.push(this._keyStr.charAt(63 & (v >> 18)));
				a.push(this._keyStr.charAt(63 & (v >> 12)));
				a.push(this._keyStr.charAt(63 & (v >> 6)));
				a.push('=');
		} else if((u.length - i) == 1) {
				var v = (u[i] << 16);
				a.push(this._keyStr.charAt(63 & (v >> 18)));
				a.push(this._keyStr.charAt(63 & (v >> 12)));
				a.push('==');
		}

		return a.join('');
	},

	_decodeArray: function(s) {
		var i = 0;
		var u = [];
		var n = 0 | (s.length / 4);
		var a = [];
		for(var j = 0; j < this._keyStr.length; ++j) {
			a[this._keyStr.charCodeAt(j)] = j;
		}
		a['='.charCodeAt(0)] = 0;

		while(0 < n--) {
			var v = (a[s.charCodeAt(i)] << 18) + (a[s.charCodeAt(i + 1)] << 12) + (a[s.charCodeAt(i + 2)] << 6) + a[s.charCodeAt(i + 3)];
			i += 4;
			u.push(255 & (v >> 16));
			u.push(255 & (v >> 8));
			u.push(255 & v);
		}
		if(u) {
			if(s.charAt(i - 2) == '=') {
				u.pop();
				u.pop();
			} else if(s.charAt(i - 1) == '=') {
				u.pop();
			}
		}

		return u;
	},

	// private method for UTF-8 encoding
	_utf8_encode: function(s) {
		var u = [];
		for(var i = 0; i < s.length; ++i) {
			var c = s.charCodeAt(i);
			if(c < 0x80) {
				u.push(c);
			} else if(c < 0x800) {
				u.push(0xC0 | (c >> 6));
				u.push(0x80 | (63 & c));
			} else if(c < 0x10000) {
				u.push(0xE0 | (c >> 12));
				u.push(0x80 | (63 & (c >> 6)));
				u.push(0x80 | (63 & c));
			} else {
				u.push(0xF0 | (c >> 18));
				u.push(0x80 | (63 & (c >> 12)));
				u.push(0x80 | (63 & (c >> 6)));
				u.push(0x80 | (63 & c));
			}
		}

		return u;
	},

	// private method for UTF-8 decoding
	_utf8_decode: function(u) {
		var a = [];
		var i = 0;
		while(i < u.length) {
			var v = u[i++];
			if(v < 0x80) {
					// no need to mask byte
			} else if(v < 0xE0) {
					v = (31 & v) << 6;
					v |= (63 & u[i++]);
			} else if(v < 0xF0) {
					v = (15 & v) << 12;
					v |= (63 & u[i++]) << 6;
					v |= (63 & u[i++]);
			} else {
					v = (7 & v) << 18;
					v |= (63 & u[i++]) << 12;
					v |= (63 & u[i++]) << 6;
					v |= (63 & u[i++]);
			}
			a.push(String.fromCharCode(v));
		}

		return a.join('');
	}
}
