/**
 * @author   RubaXa  <trash@rubaxa.org>
 * @licence  MIT
 * Обязательно нужен JSON и Promise
 */

/*global require, define, window*/
(function (factory) {
	'use strict';

	/* istanbul ignore next */
	if (typeof define === 'function' && define.amd) {
		define( factory );
	} else {
		window['Ply'] = factory();
	}
})(
	function () {
		'use strict';

		var gid = 1,
		noop    = function () {
		},

		env        = window,
		document   = env.document,
		setTimeout = env.setTimeout,

		lang = /**
		 * @desc Языковые константы
		 */

			{
				ok: 'OK',
				cancel: 'Cancel',
				cross: '✖'
		},
		support = /**
		 * @desc Что поддерживает браузер
		 */

			/*global _buildDOM*/
			(function () {
				var props  = {},
					style  = document.createElement( 'div' ).style,
					names  = 'opacity transition transform perspective transformStyle transformOrigin backfaceVisibility'.split( ' ' ),
					prefix = ['Webkit', 'Moz', 'O', 'MS'];

				_each(
					names,
					function (name, i) {
						props[name] = (name in style) && /* istanbul ignore next */ name;

						/* istanbul ignore else */
						if ( ! props[name]) {
							for (i = 0; i < 4; i++) {
								var pname = prefix[i] + name.charAt( 0 ).toUpperCase() + name.substr( 1 );
								/* istanbul ignore else */
								if (props[name] = (pname in style) && pname) {
									break;
								}
							}
						}
					}
				);

				return props;
			})(),

		/**
		 * Коды кнопок
		 * @type {Object}
		 */
		keys = /**
		 * Коды кнопок
		 * @type {Object}
		 */
			{
				esc: 27
		},

		_plyAttr = 'data-ply';

		//
		// $-like object
		//
		var $ = /**
	 * @desc $-like adapter
	 */
			window.jQuery
			|| /* istanbul ignore next */ window.Zepto
			|| /* istanbul ignore next */ window.ender
			|| /* istanbul ignore next */ window.$

		//
		//       Настройки по умолчанию
		//
		var _defaults = /**
	 * @desc Настройки по умолчанию
	 */

			{
				clone: true,
				zIndex: 10000,
				rootTag: 'form',
				baseHtml: true,

				layer: {}, // css
				wrapper: {}, // css

				overlay: {
					opacity: .6,
					backgroundColor: 'rgb(0, 0, 0)'
				},

				flags: {
					closeBtn: true,
					bodyScroll: false,
					closeByEsc: true,
					closeByOverlay: true,
					hideLayerInStack: true,
					visibleOverlayInStack: false
				},

				// Callbacks
				oninit: noop,
				onopen: noop,
				onclose: noop,
				ondestroy: noop,
				onaction: noop
		}

		//
		//       Обещания, Утилиты, CSS, DOM
		//
		/**
		 * @desc «Обещания»
		 */

		var NativePromise = window.Promise,
		Promise           = window.Deferred || NativePromise;

		/**
		 * Создать «Обещание»
		 * @param   {Function}  executor
		 * @returns {Promise}
		 * @private
		 */
		function _promise(executor) {
			/* istanbul ignore if */
			if (Promise) {
				return new Promise( executor );
			} else {
				var dfd = $.Deferred();
				executor( dfd.resolve, dfd.reject );
				return dfd;
			}
		}

		/**
		 * Дождаться разрешения всех «Обещаний»
		 * @param   {Promise[]}  iterable
		 * @returns {Promise}
		 * @private
		 */
		function _promiseAll(iterable) {
			return Promise
			   ? /* istanbul ignore next */ Promise.all( iterable )
			   : $.when.apply( $, iterable );
		}

		/**
		 * Вернуть разрешенное «Обещание»
		 * @param   {*} [value]
		 * @returns {Promise}
		 * @private
		 */
		function _resolvePromise(value) {
			return _promise(
				function (resolve) {
					return resolve( value )
				}
			);
		}

		/**
		 * Привести значение к «Обещанию»
		 * @param   {*} value
		 * @returns {Promise}
		 * @private
		 */
		function _cast(value) {
			/* istanbul ignore next */
			return value && value.then ? value : _resolvePromise( value );
		}

		//
		// Проверяем поддержку методы: done, fail, always
		//
		var __promise__ = _resolvePromise();

		/* istanbul ignore next */
		if (NativePromise && ! __promise__.always) {
			Promise = function (executor) {
				var promise       = new NativePromise( executor );
				promise.__proto__ = this.__proto__;
				return promise;
			};

			Promise.prototype             = Object.create( NativePromise.prototype );
			Promise.prototype.constructor = Promise;

			Promise.prototype.then = function (doneFn, failFn) {
				var promise       = NativePromise.prototype.then.call( this, doneFn, failFn );
				promise.__proto__ = this.__proto__; // for FireFox
				return promise;
			};

			Promise.prototype.done = function (callback) {
				this.then( callback );
				return this;
			};

			Promise.prototype.fail = function (callback) {
				this['catch']( callback );
				return this;
			};

			Promise.prototype.always = function (callback) {
				this.then( callback, callback );
				return this;
			};

			['all', 'cast', 'reject', 'resolve'].forEach(
				function (name) {
					Promise[name] = NativePromise[name];
				}
			);
		}

		/**
		 * @desc Вспомогательные методы
		 */

		/**
		 * Функция?
		 * @param   {*}  fn
		 * @returns {Boolean}
		 */
		function isFn(fn) {
			return typeof fn === 'function';
		}

		/**
		 * Object iterator
		 * @param  {Object|Array}  obj
		 * @param  {Function}      iterator
		 * @private
		 */
		function _each(obj, iterator) {
			if (obj) {
				for (var key in obj) {
					/* istanbul ignore else */
					if (obj.hasOwnProperty( key )) {
						iterator( obj[key], key, obj );
					}
				}
			}
		}

		/**
		 * Глубокое клонирование
		 * @param   {*} obj
		 * @returns {*}
		 * @private
		 */
		function _deepClone(obj) {
			var result = {};

			_each(
				obj,
				function (val, key) {
					if (isFn( val )) {
						result[key] = val;
					} else if (val instanceof Object) {
						result[key] = _deepClone( val );
					} else {
						result[key] = val;
					}
				}
			);

			return result;
		}

		/**
		 * Перенос свойств одного объект к другому
		 * @param   {Object}     dst
		 * @param   {...Object}  src
		 * @returns {Object}
		 * @private
		 */
		function _extend(dst) {
			var SLICE$0 = Array.prototype.slice;
			var src     = SLICE$0.call( arguments, 1 );
			var i       = 0, n = src.length;
			for (; i < n; i++) {
				_each(
					src[i],
					function (val, key) {
						dst[key] = val;
					}
				);
			}

			return dst;
		}

		/**
		 * @desc Работа с DOM
		 */

		/**
		 * Разбор строки "tag#id.foo.bar"
		 * @const {RegExp}
		 */
		var R_SELECTOR = /^(\w+)?(#\w+)?((?:\.[\w_-]+)*)/i;

		/**
		 * Выбрать элементы по заданному селектору
		 * @param   {String}       selector
		 * @param   {HTMLElement}  [ctx]
		 * @returns {HTMLElement}
		 */
		function _querySelector(selector, ctx) {
			try {
				return (ctx || document).querySelector( selector );
			} catch (err) {
				/* istanbul ignore next */
				return $( selector, ctx )[0];
			}
		}

		/**
		 * Найти элементы по имени
		 * @param   {HTMLElement}  el
		 * @param   {String}       name
		 * @returns {NodeList}
		 */
		function _getElementsByTagName(el, name) {
			return el.getElementsByTagName( name );
		}

		/**
		 * Присоединить элемент
		 * @param  {HTMLElement}  parent
		 * @param  {HTMLElement}  el
		 * @private
		 */
		function _appendChild(parent, el) {
			try {
				parent && el && parent.appendChild( el );
			} catch (e) {
			}
		}

		/**
		 * Удалить элемент
		 * @param  {HTMLElement}  el
		 * @private
		 */
		function _removeElement(el) {
			/* istanbul ignore else */
			if (el && el.parentNode) {
				el.parentNode.removeChild( el );
			}
		}

		/**
		 * Добавить слуашетеля
		 * @param    {HTMLElement}  el
		 * @param    {String}   name
		 * @param    {Function} fn
		 * @private
		 */
		function _addEvent(el, name, fn) {
			var handle = fn.handle = fn.handle || (function (evt) {
				/* istanbul ignore if */
				if ( ! evt.target) {
					evt.target = evt.srcElement || document;
				}

				/* istanbul ignore if */
				if (evt.target.nodeType === 3) {
					evt.target = evt.target.parentNode;
				}

				/* istanbul ignore if */
				if ( ! evt.preventDefault) {
					evt.preventDefault = function () {
						evt.returnValue = false;
					};
				}

				/* istanbul ignore if */
				if ( ! evt.stopPropagation) {
					evt.stopPropagation = function () {
						evt.cancelBubble = true;
					};
				}

				fn.call( el, evt );
			});

			/* istanbul ignore else */
			if (el.addEventListener) {
				el.addEventListener( name, handle, false );
			} else {
				el.attachEvent( 'on' + name, handle );
			}
		}

		/**
		 * Удалить слуашетеля
		 * @param    {HTMLElement}  el
		 * @param    {String}   name
		 * @param    {Function} fn
		 * @private
		 */
		function _removeEvent(el, name, fn) {
			var handle = fn.handle;
			if (handle) {
				/* istanbul ignore else */
				if (el.removeEventListener) {
					el.removeEventListener( name, handle, false );
				} else {
					el.detachEvent( 'on' + name, handle );
				}
			}
		}

		/**
		 * Создание DOM структуры по спецификации
		 * @param   {String|Object|HTMLElement}  [spec]
		 * @returns {HTMLElement}
		 * @private
		 */
		function _buildDOM(spec) {
			if (spec == null) {
				spec = 'div';
			}

			if (spec.appendChild) {
				return spec;
			} else if (spec.skip) {
				return document.createDocumentFragment();
			}

			if (typeof spec === 'string') { // selector
				spec = {tag: spec};
			}

			var el,
			children = spec.children,
			selector = R_SELECTOR.exec( spec.tag || '' );

			// Это нам больше не нужно
			delete spec.children;

			// Разбираем селектор
			spec.tag = selector[1] || 'div';
			spec.id  = spec.id || (selector[2] || '').substr( 1 );

			// Собираем className
			selector       = (selector[3] || '').split( '.' );
			selector[0]    = (spec.className || '');
			spec.className = selector.join( ' ' );

			// Создаем элемент, теперь можно
			el = document.createElement( spec.tag );
			delete spec.tag;

			// Определяем свойсва
			_each(
				spec,
				function (value, name) {
					if (value) {
						if (name === 'css') {
							// Определяем CSS свойства
							_css( el, spec.css );
						} else if (name === 'text') {
							(value != null) && _appendChild( el, document.createTextNode( value ) );
						} else if (name === 'html') {
							(value != null) && (el.innerHTML = value);
						} else if (name === 'ply') {
							// Ply-аттрибут
							el.setAttribute( _plyAttr, value );
						} else if (name in el) {
							try {
								el[name] = value;
							} catch (e) {
								el.setAttribute( name, value );
							}
						} else if (/^data-/.test( name )) {
							el.setAttribute( name, value );
						}
					}
				}
			);

			// Детишки
			if (children && children.appendChild) {
				_appendChild( el, children );
			} else {
				_each(
					children,
					function (spec, selector) {
						if (spec) {
							if (typeof spec === 'string') {
								spec = {text: spec};
							} else if (typeof spec !== 'object') {
								spec = {};
							}

							/* istanbul ignore else */
							if (typeof selector === 'string') {
								spec.tag = spec.tag || selector;
							}

							_appendChild( el, _buildDOM( spec ) );
						}
					}
				);
			}

			return el;
		}

		/**
		 * Выбрать первый не заполненый элемент
		 * @param   {HTMLElement}  parentNode
		 * @private
		 */
		function _autoFocus(parentNode) {
			var items = _getElementsByTagName( parentNode, 'input' ),
			i         = 0,
			n         = items.length,
			el,
			element;

			for (; i < n; i++) {
				el = items[i];

				/* istanbul ignore else */
				if (el.type === 'submit') {
					! element && (element = el);
				} else if ( ! /hidden|check|radio/.test( el.type ) && el.value == '') {
					element = el;
					break;
				}
			}

			if ( ! element) {
				element = _getElementsByTagName( parentNode, 'button' )[0];
			}

			try {
				element.focus();
			} catch (err) {
			}
		}

		/**
		 * Предзагрузить все изображения
		 * @param   {HTMLElement}  parentNode
		 * @returns {Promise}
		 * @private
		 */
		function _preloadImage(parentNode) {
			_loading( true );

			return _promise(
				function (resolve) {
					var items = _getElementsByTagName( parentNode, 'img' ),
					i         = items.length,
					queue     = i,
					img,
					complete  = function () {
						/* istanbul ignore else */
						if (--queue <= 0) {
							i = items.length;
							while (i--) {
								img = items[i];
								_removeEvent( img, 'load', complete );
								_removeEvent( img, 'error', complete );
							}
							_loading( false );
							resolve();
						}
					};

					while (i--) {
						img = items[i];
						if (img.complete) {
							queue--;
						} else {
							_addEvent( img, 'load', complete );
							_addEvent( img, 'error', complete );
						}
					}

					! queue && complete();
				}
			);
		}

		/**
		 * @desc Функция для работы с style
		 */

		/*global support, document*/

		/**
		 * Хуки для css
		 * @type {Object}
		 */
		var _cssHooks = {
			opacity: ! support.opacity && function (style, value) {
				style.zoom   = 1;
				style.filter = 'alpha(opacity=' + (value * 100) + ')';
			}
		};

		/**
		 * Установка или получение css свойства
		 * @param   {HTMLElement}    el
		 * @param   {Object|String}  prop
		 * @param   {String}         [val]
		 * @returns {*}
		 * @private
		 */
		function _css(el, prop, val) {
			if (el && el.style && prop) {
				if (prop instanceof Object) {
					for (var name in prop) {
						_css( el, name, prop[name] );
					}
				} else if (val === void 0) {
					/* istanbul ignore else */
					if (document.defaultView && document.defaultView.getComputedStyle) {
						val = document.defaultView.getComputedStyle( el, '' );
					} else if (el.currentStyle) {
						val = el.currentStyle;
					}

					return prop === void 0 ? val : val[prop];
				} else if (_cssHooks[prop]) {
					_cssHooks[prop]( el.style, val );
				} else {
					el.style[support[prop] || prop] = val;
				}
			}
		}

		/**
		 * «Загрузка»
		 * @param  {Boolean}  state
		 * @private
		 */
		function _loading(state) {
			var el = _loading.get();

			clearTimeout( _loading.pid );
			if (state) {
				_loading.pid = setTimeout(
					function () {
						_appendChild( document.body, el );
					},
					100
				);
			} else {
				_loading.pid = setTimeout(
					function () {
						_removeElement( el );
					},
					100
				);
			}
		}

		/**
		 * Получить ссылку на элемент loading
		 * @returns {HTMLElement}
		 */
		_loading.get = function () {
			return _loading.el || (_loading.el = _buildDOM(
				{
					tag: '.ply-global-loading',
					children: {'.ply-loading-spinner': true}
				}
			));
		};

		/**
		 * Создать слой с контентом
		 * @param   {HTMLElement} contentEl
		 * @param   {Object}      options
		 * @returns {HTMLElement}
		 * @private
		 */
		function _createLayer(contentEl, options) {
			return _buildDOM(
				{
					css: _extend(
						{
							padding: '20px 20px 40px', // Сницу в два раза больше, так лучше
							display: 'inline-block',
							position: 'relative',
							textAlign: 'left',
							whiteSpace: 'normal',
							verticalAlign: 'middle',
							transform: 'translate3d(0, 0, 0)'
						},
						options.wrapper
					),
				children: options.baseHtml ? [{
					ply: ':layer',
					tag: '.ply-layer',
					className: options.mod,
					css: _extend(
						{
							overflow: 'hidden',
							position: 'relative',
							backfaceVisibility: 'hidden'
						},
						options.layer
					),
					children: [options.flags.closeBtn && {
						ply: ':close',
						tag: '.ply-x',
						text: lang.cross
					}, {
						tag: '.ply-inside',
						children: contentEl
					}]
					}] : contentEl
				}
			);
		}

		/**
		 * Создать затемнение
		 * @param   {Object}   style
		 * @returns {HTMLElement}
		 * @private
		 */
		function _createOverlay(style) {
			return _buildDOM(
				{
					ply: ':overlay',
					tag: '.ply-overlay',
					css: {
						top: 0,
						left: 0,
						right: 0,
						bottom: 0,
						position: 'fixed'
					},
					children: [{tag: 'div', css: _extend( {width: '100%', height: '100%'}, style )}]
				}
			);
		}

		/**
		 * Создать ply—объвязку
		 * @param   {Object}   target
		 * @param   {Object}   options
		 * @param   {Boolean}  [onlyLayer]
		 * @returns {Object}
		 * @private
		 */
		function _createPly(target, options, onlyLayer) {
			// Корневой слой
			target.wrapEl = _buildDOM(
				{
					tag: options.rootTag,
					css: {whiteSpace: 'nowrap', zIndex: options.zIndex},
					method: 'post',
					action: '/'
				}
			);

			// Затемнение
			if ( ! onlyLayer) {
				target.overlayEl    = _createOverlay( options.overlay );
				target.overlayBoxEl = target.overlayEl.firstChild;
				_appendChild( target.wrapEl, target.overlayEl );
			}

			// Пустышка для центрирования по вертикали
			var dummyEl = _buildDOM();
			_css(
				dummyEl,
				{
					height: '100%',
					display: 'inline-block',
					verticalAlign: 'middle'
				}
			);
			_appendChild( target.wrapEl, dummyEl );

			// Контент
			var el    = options.el;
			target.el = (el && el.cloneNode) ? (options.clone ? el.cloneNode( true ) : el) : _buildDOM( {html : el || ''} );

			// Содержит контент
			target.layerEl   = _createLayer( target.el, options );
			target.contentEl = _getContentEl( target.layerEl );
			target.context   = new Context( target.layerEl );

			_appendChild( target.wrapEl, target.layerEl );

			// Родительский элемент
			target.bodyEl = options.body && _querySelector( options.body ) || document.body;

			target.wrapEl.tabIndex = -1; // для фокуса
			_css(
				target.wrapEl,
				{
					top: 0,
					left: 0,
					right: 0,
					bottom: 0,
					position: 'fixed',
					textAlign: 'center',
					overflow: 'auto',
					outline: 0
				}
			);

			return target;
		}

		/**
		 * Получить ссылку на контент
		 * @param   {HTMLElement}  layerEl
		 * @returns {HTMLElement}
		 * @private
		 */
		function _getContentEl(layerEl) {
			return layerEl.firstChild.lastChild.firstChild;
		}

		//
		//       Основной код
		//

		/**
		 * @class   Ply
		 * @param   {HTMLElement|Object}   el  слой или опции
		 * @param   {Object}               [options]   опции слоя
		 */
		function Ply(el, options) {
			options    = (el instanceof Object) ? el : (options || {});
			options.el = options.el || el;

			var _this = this;

			// Локальный идентификатор
			_this.cid = 'c' + gid++;

			// Увеличиваем глобальный zIndex
			_defaults.zIndex++;

			// Опции
			_this.options = options = _extend( {}, _defaults, options );

			// Флаги
			options.flags = _extend( {}, _defaults.flags, options.flags );

			// Создаем Ply-элементы
			_createPly( _this, options );

			// Установим эффекты
			_this.setEffect( options.effect );

			// Очередь эффектов
			_this.fx       = function (executor) {
				/* jshint boss:true */
				return ! (_this.fx.queue = _this.fx.queue.then( executor, executor ).then(
					function () {
						return _this;
					}
				));
			};
			_this.fx.queue = _resolvePromise();

			// Клик по затемнению
			_this.on(
				'click',
				':overlay',
				function () {
					_this.hasFlag( 'closeByOverlay' ) && _this.closeBy( 'overlay' );
				}
			);

			// Подписываемся кнопку «отмена» и «крестик»
			_this.on(
				'click',
				':close',
				function (evt, el) {
					evt.preventDefault();
					_this.closeBy( el.nodeName === 'BUTTON' ? 'cancel' : 'x' );
				}
			);

			// Событие инициализации
			_this.options.oninit( this );
		}

		// Методы
		Ply.fn = Ply.prototype = /** @lends Ply.prototype */ {
			constructor: Ply,

			/** @private */
			_activate: function () {
				if ( ! this.hasFlag( 'bodyScroll' )) {
					var bodyEl = this.bodyEl,
					dummyEl    = _buildDOM(
						{
							css: {overflow: 'auto', visibility: 'hidden', height: '5px'},
							children: [{tag: 'div', css: {height: '100px'}}]
						}
					);

					// @todo: Покрыть тестами
					// Сохраняем оригинальные значения
					this.__overflow     = _css( bodyEl, 'overflow' );
					this.__paddingRight = _css( bodyEl, 'paddingRight' );

					_appendChild( bodyEl, dummyEl );
					_css(
						bodyEl,
						{
							overflow: 'hidden',
							paddingRight: (dummyEl.offsetWidth - dummyEl.firstChild.offsetWidth) + 'px'
						}
					);
					_removeElement( dummyEl );
				}

				_addEvent( this.wrapEl, 'submit', this._getHandleEvent( 'submit' ) );
			},

			/** @private */
			_deactivate: function () {
				if ( ! this.hasFlag( 'bodyScroll' )) {
					_css(
						this.bodyEl,
						{
							overflow: this.__overflow,
							paddingRight: this.__paddingRight
						}
					);
				}

				_removeEvent( this.layerEl, 'submit', this._getHandleEvent( 'submit' ) );
			},

			/**
			 * Получить обработчик события
			 * @param   {String}  name  событие
			 * @returns {*}
			 * @private
			 */
			_getHandleEvent: function (name) {
				var _this   = this,
				handleEvent = _this.__handleEvent || (_this.__handleEvent = {});

				if ( ! handleEvent[name]) {
					handleEvent[name] = function (evt) {
						_this._handleEvent( name, evt );
					};
				}

				return handleEvent[name];
			},

			/**
			 * Центральный обработчик события
			 * @param   {String}  name
			 * @param   {Event}   evt
			 * @private
			 */
			_handleEvent: function (name, evt) {
				evt.preventDefault();
				this.closeBy( name );
			},

			/**
			 * jQuery выборка из слоя
			 * @param   {String}  selector
			 * @returns {jQuery}
			 */
			$: function (selector) {
				return $( selector, this.layerEl );
			},

			/**
			 * Найти элемент внутри слоя
			 * @param   {String}  selector
			 * @returns {HTMLElement}
			 */
			find: function (selector) {
				return _querySelector( selector, this.layerEl );
			},

			/**
			 * Применить эффект к элементу
			 * @param   {HTMLElement}    el
			 * @param   {String}         name
			 * @param   {String|Object}  [effects]
			 * @returns {Promise}
			 */
			applyEffect: function (el, name, effects) {
				el = this[el] || el;

				if ( ! el.nodeType) {
					effects = name;
					name    = el;
					el      = this.layerEl;
				}

				effects = Ply.effects.get( effects || this.effects );
				return Ply.effects.apply.call( effects, el, name );
			},

			/**
			 * Закрыть «по»
			 * @param  {String}  name  прчина закрытия
			 */
			closeBy: function (name) {
				var this$0 = this;
				var ui     = {
					by: name,
					state: name === 'submit',
					data: this.context.toJSON(),
					widget: this,
					context: this.context
				},
				el         = this.el,
				result     = this.options.onaction( ui );

				if ( ! this.__lock) {
					this.__lock        = true;
					this.el.className += ' ply-loading';

					_cast( result )
					.done(
						function (state) {
							if (state !== false) {
								this$0.result = ui;
								this$0.close();
							}
						}
					)
						.always(
							function () {
								this$0.__lock       = false;
								this$0.el.className = this$0.el.className.replace( /\s?ply-loading/, '' );
							}
						);
				}
			},

			/**
			 * Подписаться на ply-событие
			 * @param   {String}    event   событие
			 * @param   {String}    target  ply-selector
			 * @param   {Function}  handle
			 * @returns {Ply}
			 */
			on: function (event, target, handle) {
				var _this = this;

				if ( ! handle) {
					handle = target;
					target = ':layer';
				}

				handle['_' + target] = function (evt) {
					var el = evt.target;

					do {
						if (el.nodeType === 1) {
							if (el.getAttribute( _plyAttr ) === target) {
								return handle.call( _this, evt, el );
							}
						}
					} while ((el !== _this.wrapEl) && (el = el.parentNode));
				};

				_addEvent( _this.wrapEl, event, handle['_' + target] );
				return _this;
			},

			/**
			 * Отписаться от ply-событие
			 * @param   {String}    event   событие
			 * @param   {String}    target  ply-selector
			 * @param   {Function}  handle
			 * @returns {Ply}
			 */
			off: function (event, target, handle) {
				if ( ! handle) {
					handle = target;
					target = 'layer';
				}

				_removeEvent( this.wrapEl, event, handle['_' + target] || noop );
				return this;
			},

			/**
			 * Проверить наличие флага
			 * @param   {String}  name  имя флага
			 * @returns {Boolean}
			 */
			hasFlag: function (name) {
				return ! ! this.options.flags[name];
			},

			/**
			 * Установить effect
			 * @param   {String|Object}  name
			 * @returns {Ply}
			 */
			setEffect: function (name) {
				this.effects = Ply.effects.get( name );
				return this;
			},

			/**
			 * Открыть/закрыть слой
			 * @param   {Boolean}  state
			 * @param   {*}  effect
			 * @returns {Promise}
			 * @private
			 */
			_toggleState: function (state, effect) {
				var _this = this,
				mode      = state ? 'open' : 'close',
				prevLayer = Ply.stack.last;

				/* istanbul ignore else */
				if (_this.visible != state) {
					_this.visible = state;
					_this[state ? '_activate' : '_deactivate']();

					// Добавить или удалить слой из стека
					Ply.stack[state ? 'add' : 'remove']( _this );

					// Очередь эффектов
					_this.fx(
						function () {
							return _preloadImage( _this.wrapEl ).then(
								function () {
									var isFirst = Ply.stack.length === (state ? 1 : 0),
									hideLayer   = prevLayer && prevLayer.hasFlag( 'hideLayerInStack' ),
									hasOverlay  = isFirst || _this.hasFlag( 'visibleOverlayInStack' );

									if (state) {
										// Убрать «затемнение» если мы не первые в стеке
										! hasOverlay && _removeElement( _this.overlayBoxEl );

										_appendChild( _this.bodyEl, _this.wrapEl );
										_this.wrapEl.focus();
										_autoFocus( _this.layerEl );

										if (hideLayer) {
											// Скрыть слой «под»
											prevLayer.applyEffect( 'close.layer', effect ).then(
												function () {
													_removeElement( prevLayer.layerEl );
												}
											);
										}
									} else if (prevLayer = Ply.stack.last) {
										// Слой мог быть скрыт, нужно вернуть его
										_appendChild( prevLayer.wrapEl, prevLayer.layerEl );
										prevLayer.hasFlag( 'hideLayerInStack' ) && prevLayer.applyEffect( 'open.layer', effect ).then(
											function () {
												_autoFocus( prevLayer.el ); // todo: нужен метод layer.autoFocus();
											}
										);
									}

									// Применяем основные эффекты
									return _promiseAll(
										[
										_this.applyEffect( mode + '.layer', effect ),
										hasOverlay && _this.applyEffect( 'overlayEl', mode + '.overlay', effect )
										]
									).then(
										function () {
											if ( ! state) {
												_removeElement( _this.wrapEl );
												_appendChild( _this.overlayEl, _this.overlayBoxEl );
											}
											// «Событие» open или close
											_this.options['on' + mode]( _this );
										}
									);
								}
							);
						}
					);
				}

				return _this.fx.queue;
			},

			/**
			 * Открыть слой
			 * @param   {*}  [effect]
			 * @returns {Promise}
			 */
			open: function (effect) {
				this.result = null;
				return this._toggleState( true, effect );
			},

			/**
			 * Закрыть слой
			 * @param   {*}  [effect]
			 * @returns {Promise}
			 */
			close: function (effect) {
				return this._toggleState( false, effect );
			},

			/**
			 * @param   {HTMLElement}  closeEl
			 * @param   {HTMLElement}  openEl
			 * @param   {Object}    effects
			 * @param   {Function}  prepare
			 * @param   {Function}  [complete]
			 * @returns {*}
			 * @private
			 */
			_swap: function (closeEl, openEl, effects, prepare, complete) {
				var _this = this;

				if (_this.visible) {
					_this.fx(
						function () {
							return _preloadImage( openEl ).then(
								function () {
									prepare();

									return _promiseAll(
										[
										_this.applyEffect( closeEl, 'close.layer', effects ),
										_this.applyEffect( openEl, 'open.layer', effects )
										]
									).then(
										function () {
											_removeElement( closeEl );
											complete();
											_this.wrapEl.focus();
											_autoFocus( openEl );
										}
									);
								}
							);
						}
					);
				} else {
					complete();
				}

				return _this.fx.queue;
			},

			/**
			 * Заменить слой
			 * @param   {Object}  options
			 * @param   {Object}  [effect]  эффект замены
			 * @returns {Promise}
			 */
			swap: function (options, effect) {
				options = _extend( {}, this.options, options );

				var _this = this,
				ply       = _createPly( {}, options, true ),
				effects   = (effect || options.effect) ? Ply.effects.get( effect || options.effect ) : _this.effects,
				closeEl   = _this.layerEl,
				openEl    = ply.layerEl;

				return _this._swap(
					closeEl,
					openEl,
					effects,
					function () {
						_appendChild( _this.bodyEl, _this.wrapEl );
						_appendChild( _this.bodyEl, ply.wrapEl );
					},
					function () {
						_removeElement( ply.wrapEl );
						_appendChild( _this.wrapEl, openEl );

						_this.el         = ply.el;
						_this.layerEl    = openEl;
						_this.contentEl  = _getContentEl( openEl );
						_this.context.el = openEl;
					}
				);
			},

			/**
			 * Заменить внутренности слоя
			 * @param   {Object}  options
			 * @param   {Object}  [effect]  эффект замены
			 * @returns {Promise}
			 */
			innerSwap: function (options, effect) {
				options = _extend( {}, this.options, options );

				var _this = this,
				ply       = _createPly( {}, options, true ),
				effects   = (effect || options.effect) ? Ply.effects.get( effect || options.effect ) : _this.effects,

				inEl  = _querySelector( '.ply-inside', ply.layerEl ),
				outEl = _querySelector( '.ply-inside', _this.layerEl );

				return _this._swap(
					outEl,
					inEl,
					effects,
					function () {
						_css( outEl, {width: outEl.offsetWidth + 'px', position: 'absolute'} );
						_appendChild( outEl.parentNode, inEl );
					},
					noop
				);
			},

			/**
			 * Уничтожить слой
			 */
			destroy: function () {
				_removeElement( this.wrapEl );

				this._deactivate();
				Ply.stack.remove( this );

				this.visible = false;
				this.options.ondestroy( this );
			}
		};

		//
		// Ply-стек
		//
		/**
		 * @desc Работы со стеком
		 */

		var array_core = [],
		array_push     = array_core.push,
		array_splice   = array_core.splice;

		Ply.stack = {
			_idx: {},

			/**
			 * Последний Ply в стеке
			 * @type {Ply}
			 */
			last: null,

			/**
			 * Длинна стека
			 * @type {Number}
			 */
			length: 0,

			/**
			 * Удаить последний ply-слой из стека
			 * @param  {Event}  evt
			 * @private
			 */
			_pop: function (evt) {
				var layer = Ply.stack.last;

				if (evt.keyCode === keys.esc && layer.hasFlag( 'closeByEsc' )) {
					layer.closeBy( 'esc' );
				}
			},

			/**
			 * Добавить ply в стек
			 * @param  {Ply}  layer
			 */
			add: function (layer) {
				var idx = array_push.call( this, layer );

				this.last            = layer;
				this._idx[layer.cid] = idx - 1;

				if (idx === 1) {
					_addEvent( document, 'keyup', this._pop );
				}
			},

			/**
			 * Удалить ply из стека
			 * @param  {Ply}  layer
			 */
			remove: function (layer) {
				var idx = this._idx[layer.cid];

				if (idx >= 0) {
					array_splice.call( this, idx, 1 );

					delete this._idx[layer.cid];
					this.last = this[this.length - 1];

					if ( ! this.last) {
						_removeEvent( document, 'keyup', this._pop );
					}
				}
			}
		};

		//
		// Эффекты
		//
		Ply.effects = /**
	 * @desc Объект для работы с эффектами
	 */

		{
			// Установки по умолчанию
			defaults: {
				duration: 300,

				open: {
					layer: null,
					overlay: null
				},

				close: {
					layer: null,
					overlay: null
				}
			},

			/**
			 * Настройти эффекты по умолчанию
			 * @static
			 * @param  {Object}  options
			 */
			setup: function (options) {
				this.defaults = this.get( options );
			},

			set: function (desc) {
				_extend( this, desc );
			},

			/**
			 * Получить опции на основе переданных и по умолчанию
			 * @static
			 * @param   {Object}  options  опции
			 * @returns {Object}
			 */
			get: function (options) {
				var defaults = _deepClone( this.defaults );

				// Функция разбора выражения `name:duration[args]`
				function parseKey(key) {
					var match = /^([\w_-]+)(?::(\d+%?))?(\[[^\]]+\])?/.exec( key ) || [];
					return {
						name: match[1] || key,
						duration: match[2] || null,
						args: JSON.parse( match[3] || 'null' ) || {}
					};
				}

				function toObj(obj, key, def) {
					var fx       = parseKey( key ),
						val      = (obj[fx.name] || def || {}),
						duration = (fx.duration || val.duration || obj.duration || options.duration);

					if (typeof val === 'string') {
						val = parseKey( val );
						delete val.args;
					}

					if (/%/.test( val.duration )) {
						val.duration = parseInt( val.duration, 10 ) / 100 * duration;
					}

					val.duration = (val.duration || duration) | 0;

					return val;
				}

				if (typeof options === 'string') {
					var fx             = parseKey( options );
					options            = _deepClone( this[fx.name] || {open: {}, close: {}} );
					options.duration   = fx.duration || options.duration;
					options.open.args  = fx.args[0];
					options.close.args = fx.args[1];
				} else if (options instanceof Array) {
					var openFx  = parseKey( options[0] ),
						closeFx = parseKey( options[1] ),
						open    = this[openFx.name],
						close   = this[closeFx.name];

					options = {
						open: _deepClone( open && open.open || {layer: options[0], overlay: options[0]} ),
						close: _deepClone( close && close.close || {layer: options[1], overlay: options[1]} )
					};

					options.open.args  = openFx.args[0];
					options.close.args = closeFx.args[0];
				} else if ( ! (options instanceof Object)) {
					options = {};
				}

				options.duration = (options.duration || defaults.duration) | 0;

				for (var key in {open: 0, close: 0}) {
					var val = options[key] || defaults[key] || {};
					if (typeof val === 'string') {
						// если это строка, то только layer
						val = {layer: val};
					}
					val.layer   = toObj( val, 'layer', defaults[key].layer );
					val.overlay = toObj( val, 'overlay', defaults[key].overlay );

					if (val.args === void 0) {
						// clean
						delete val.args;
					}

					options[key] = val;
				}

				return options;
			},

			/**
			 * Применить эффекты
			 * @static
			 * @param   {HTMLElement}  el    элемент, к которому нужно применить эффект
			 * @param   {String}       name  название эффекта
			 * @returns {Promise|undefined}
			 */
			apply: function (el, name) {
				name = name.split( '.' );

				var effects  = this[name[0]], // эффекты open/close
					firstEl  = el.firstChild,
					oldStyle = [el.getAttribute( 'style' ), firstEl && firstEl.getAttribute( 'style' )],
					fx,
					effect;

				if (support.transition && effects && (effect = effects[name[1]]) && (fx = Ply.effects[effect.name])) {
					if (fx['to'] || fx['from']) {
						// Клонируем
						fx = _deepClone( fx );

						// Выключаем анимацию
						_css( el, 'transition', 'none' );
						_css( firstEl, 'transition', 'none' );

						// Определяем текущее css-значения
						_each(
							fx['to'],
							function (val, key, target) {
								if (val === '&') {
									target[key] = _css( el, key );
								}
							}
						);

						// Выставляем initied значения
						if (isFn( fx['from'] )) {
							fx['from']( el, effects.args );
						} else if (fx['from']) {
							_css( el, fx['from'] );
						}

						return _promise(
							function (resolve) {
								// Принудительный repaint/reflow
								fx.width = el.offsetWidth;

								// Включаем анимацию
								_css( el, 'transition', 'all ' + effect.duration + 'ms' );
								_css( firstEl, 'transition', 'all ' + effect.duration + 'ms' );

								// Изменяем css
								if (isFn( fx['to'] )) {
									fx['to']( el, effects.args );
								} else {
									_css( el, fx['to'] );
								}

								// Ждем завершения анимации
								setTimeout( resolve, effect.duration );
							}
						).then(
							function () {
								el.setAttribute( 'style', oldStyle[0] );
								firstEl && firstEl.setAttribute( 'style', oldStyle[1] );
							}
						);
					}
				}

				return _resolvePromise();
			}
		}
		Ply.effects.set(/**
		 * @desc Предустановленные эффекты
		 */

			{
				//
				// Комбинированный эффекты
				//

				'fade': {
					open: {layer: 'fade-in:80%', overlay: 'fade-in:100%'},
					close: {layer: 'fade-out:60%', overlay: 'fade-out:60%'}
				},

				'scale': {
					open: {layer: 'scale-in', overlay: 'fade-in'},
					close: {layer: 'scale-out', overlay: 'fade-out'}
				},

				'fall': {
					open: {layer: 'fall-in', overlay: 'fade-in'},
					close: {layer: 'fall-out', overlay: 'fade-out'}
				},

				'slide': {
					open: {layer: 'slide-in', overlay: 'fade-in'},
					close: {layer: 'slide-out', overlay: 'fade-out'}
				},

				'3d-flip': {
					open: {layer: '3d-flip-in', overlay: 'fade-in'},
					close: {layer: '3d-flip-out', overlay: 'fade-out'}
				},

				'3d-sign': {
					open: {layer: '3d-sign-in', overlay: 'fade-in'},
					close: {layer: '3d-sign-out', overlay: 'fade-out'}
				},

				'inner': {
					open: {layer: 'inner-in'},
					close: {layer: 'inner-out'}
				},

				//
				// Описание эффекта
				//

				'fade-in': {
					'from': {opacity: 0},
					'to': {opacity: '&'}
				},

				'fade-out': {
					'to': {opacity: 0}
				},

				'slide-in': {
					'from': {opacity: 0, transform: 'translateY(20%)'},
					'to': {opacity: '&', transform: 'translateY(0)'}
				},

				'slide-out': {
					'to': {opacity: 0, transform: 'translateY(20%)'}
				},

				'fall-in': {
					'from': {opacity: 0, transform: 'scale(1.3)'},
					'to': {opacity: '&', transform: 'scale(1)'}
				},

				'fall-out': {
					'to': {opacity: 0, transform: 'scale(1.3)'}
				},

				'scale-in': {
					'from': {opacity: 0, transform: 'scale(0.7)'},
					'to': {opacity: '&', transform: 'scale(1)'}
				},

				'scale-out': {
					'to': {opacity: 0, 'transform': 'scale(0.7)'}
				},

				'rotate3d': function (el, opacity, axis, deg, origin) {
					_css( el, {perspective: '1300px'} );
					_css(
						el.firstChild,
						{
							opacity: opacity,
							transform: 'rotate' + axis + '(' + deg + 'deg)',
							transformStyle: 'preserve-3d',
							transformOrigin: origin ? '50% 0' : '50%'
						}
					);
				},

				'3d-sign-in': {
					'from': function (el) {
						Ply.effects.rotate3d( el, 0, 'X', -60, '50% 0' );
					},
					'to': function (el) {
						_css( el.firstChild, {opacity: 1, transform: 'rotateX(0)'} );
					}
				},

				'3d-sign-out': {
					'from': function (el) {
						Ply.effects.rotate3d( el, 1, 'X', 0, '50% 0' );
					},
					'to': function (el) {
						_css( el.firstChild, {opacity: 0, transform: 'rotateX(-60deg)'} );
					}
				},

				'3d-flip-in': {
					'from': function (el, deg) {
						Ply.effects.rotate3d( el, 0, 'Y', deg || -70 );
					},
					'to': function (el) {
						_css( el.firstChild, {opacity: 1, transform: 'rotateY(0)'} );
					}
				},

				'3d-flip-out': {
					'from': function (el) {
						Ply.effects.rotate3d( el, 1, 'Y', 0 );
					},
					'to': function (el, deg) {
						_css( el.firstChild, {opacity: 0, transform: 'rotateY(' + (deg || 70) + 'deg)'} );
					}
				},

				'inner-in': {
					'from': function (el) {
						_css( el, 'transform', 'translateX(100%)' );
					},
					'to': function (el) {
						_css( el, 'transform', 'translateX(0%)' );
					}
				},

				'inner-out': {
					'from': function (el) {
						_css( el, 'transform', 'translateX(0%)' );
					},
					'to': function (el) {
						_css( el, 'transform', 'translateX(-100%)' );
					}
				}
			}
		);

		//
		// Ply.Context
		//
		/**
		 * @desc Ply-контекст
		 */

		/**
		 * @class  Ply.Context
		 * @param  {HTMLElement}  el
		 */
		function Context(el) {
			/**
			 * Корневой элемент
			 * @type {HTMLElement}
			 */
			this.el = el;
		}

		Context.fn = Context.prototype = /** @lends Ply.Context */{
			constructor: Context,

			/**
			 * Получить элемент по имени
			 * @param   {String}  name
			 * @returns {HTMLElement|undefined}
			 */
			getEl: function (name) {
				if (this.el) {
					return _querySelector( '[' + _plyAttr + '-name="' + name + '"]', this.el );
				}
			},

			/**
			 * Получить или установить значение по имени
			 * @param   {String}  name
			 * @param   {String}  [value]
			 * @returns {String}
			 */
			val: function (name, value) {
				var el = typeof name === 'string' ? this.getEl( name ) : name;

				if (el && (el.value == null)) {
					el = _getElementsByTagName( el, 'input' )[0]
					|| _getElementsByTagName( el, 'textarea' )[0]
					|| _getElementsByTagName( el, 'select' )[0];
				}

				if (el && value != null) {
					el.value = value;
				}

				return el && el.value || "";
			},

			/**
			 * Получить JSON
			 * @returns {Object}
			 */
			toJSON: function () {
				var items = this.el.querySelectorAll( '[' + _plyAttr + '-name]' ),
				json      = {},
				el,
				i         = items.length;
				while (i--) {
					el = items[i];
					json[el.getAttribute( _plyAttr + '-name' )] = this.val( el );
				}
				return json;
			}
		};

		//
		// Ply.ui
		//
		/**
		 * @desc Диалоги
		 */

		/*global Ply */

		'use strict';

		function _isNode(el) {
			return el && el.appendChild;
		}

		function _toBlock(block, name) {
			if (block == null) {
				return {skip: true};
			}

			if (typeof block === 'string') {
				block = {text: block};
			}

			if (typeof block === 'object') {
				block.name = block.name || name;
			}

			return block;
		}

		/**
		 * Управление рендером UI
		 * @param  {String}  name
		 * @param  {Object}  [data]
		 * @param  {String}  [path]
		 * @returns {HTMLElement}
		 */
		function ui(name, data, path) {
			var fn = ui[name], el;

			if ( ! fn) {
				name = name.split( /\s+/ ).slice( 0, -1 ).join( ' ' );
				fn   = data && (
					ui[name + ' [name=' + data.name + ']']
					|| ui[name + ' [type=' + data.type + ']']
					)
					|| ui[name + ' *']
					|| ui[':default'];
			}

			el = _buildDOM( fn( data, path ) );
			if (data && data.name) {
				el.setAttribute( _plyAttr + '-name', data.name );
			}
			el.className += ' ply-ui';

			return el;
		}

		/**
		 * Назначение визуализатор
		 * @param {String}    name  имя фабрики
		 * @param {Function}  renderer
		 * @param {Boolean}  [simpleMode]
		 */
		ui.factory = function (name, renderer, simpleMode) {
			ui[name.replace( /^\s+|\s+$/g, '' ).replace( /\s+/g, ' ' )] = function (data, path) {
				var fragment = document.createDocumentFragment(), result;

				if ((data != null) || name === ':root') {
					data = simpleMode ? data : _toBlock( data );

					_each(
						simpleMode ? data : data.children,
						function (block, key) {
							var abs = ((path || name) + ' ' + key).replace( /^:\w+\s+/, '' ),
							el      = _isNode( block ) ? block : ui( abs, _toBlock( block, key ), abs );

							_appendChild( fragment, el );
						}
					);

					if ( ! simpleMode) {
						delete data.children;
					}

					result = renderer( data, fragment );

					/* istanbul ignore else */
					if ( ! _isNode( result )) {
						_extend( result, data );
					}

					return result;
				}

				return fragment;
			};
		};

		// Элемент по умолчанию
		ui.factory(
			':default',
			function (data, children) {
				data.children = children;
				return data;
			}
		);

		// Ply-слой - корневой элемент
		ui.factory(
			':root',
			function (data) {
				return {
					tag: '.ply-form',
					className: data.mod,
					children: [
					ui( ':header', data.header ),
					ui( ':content', data.content ),
					data.ctrls && ui(
						':default',
						{
							tag: 'div.ply-footer',
							children: data.ctrls
						}
					)
				]
				};
			}
		);

		// «Заголовк» слоя
		ui.factory(
			':header',
			function (data, children) {
				return {tag: '.ply-header', text: data.text, children: children};
			}
		);

		// «Содержимое» слоя
		ui.factory(
			':content',
			function (data, children) {
				return {tag: '.ply-content', children: children};
			},
			true
		);

		// Кнопка «ОК»
		ui.factory(
			'ok',
			function (data) {
				return {
					ply: ':ok',
					tag: 'button.ply-ctrl.ply-ok',
					text: data === true ? lang.ok : data
				};
			}
		);

		// Кнопка «Отмена»
		ui.factory(
			'cancel',
			function (data) {
				return {
					ply: ':close',
					tag: 'button.ply-ctrl.ply-cancel',
					type: 'reset',
					text: data === true ? lang.cancel : data
				};
			}
		);

		/**
		 * Фабрика слоев
		 * @param {String}   name
		 * @param {Function} renderer
		 */
		function factory(name, renderer) {
			factory['_' + name] = renderer;

			factory[name] = function (options, data) {
				return _promise(
					function (resolve, reject) {
						renderer( options, data, resolve, reject );
					}
				).then(
					function (el) {
						/* istanbul ignore else */
						if ( ! _isNode( el )) {
							el = ui( ':root', el );
						}

						return el;
					}
				);
			};
		}

		/**
		 * Использовать фабрику
		 * @param {String}   name
		 * @param {Object}   options
		 * @param {Object}   data
		 * @param {Function} resolve
		 * @param {Function} [reject]
		 */
		factory.use = function (name, options, data, resolve, reject) {
			factory['_' + name]( options, data, resolve, reject );
		};

		/**
		 * Абстрактный диалог
		 * @param   {String}  mod  модификатор
		 * @param   {Object}  options
		 * @param   {Object}  data
		 * @param   {Object}  defaults
		 * @returns {Object}
		 * @private
		 */
		function _dialogFactory(mod, options, data, defaults) {
			options.mod    = mod;
			options.effect = options.effect || 'slide';
			options.flags  = _extend( {closeBtn: false}, options.flags );

			return {
				header: data.title,
				content: data.form
					 ? {'dialog-form' : {children : data.form}}
					 : {el: data.text || data},
				ctrls: {
					ok: data.ok || defaults.ok,
					cancel: data.cancel === false ? null : (data.cancel || defaults.cancel)
				}
			};
		}

		// Фабрика по умолчанию
		factory(
			'default',
			function (options, data, resolve) {
				resolve( data || /* istanbul ignore next */ {} );
			}
		);

		// Базовый жиалог
		factory(
			'base',
			function (options, data, resolve) {
				resolve( _dialogFactory( 'base', options, data ) );
			}
		);

		// Диалог: «Предупреждение»
		factory(
			'alert',
			function (options, data, resolve) {
				resolve( _dialogFactory( 'alert', options, data, {ok: true} ) );
			}
		);

		// Диалог: «Подтверждение»
		factory(
			'confirm',
			function (options, data, resolve) {
				resolve(
					_dialogFactory(
						'confirm',
						options,
						data,
						{
							ok: true,
							cancel: true
						}
					)
				);
			}
		);

		// Диалог: «Запросить данные»
		factory(
			'prompt',
			function (options, data, resolve) {
				resolve(
					_dialogFactory(
						'prompt',
						options,
						data,
						{
							ok: true,
							cancel: true
						}
					)
				);
			}
		);

		// Элемент формы
		ui.factory(
			'dialog-form *',
			function (data) {
				return {
					tag: 'input.ply-input',
					type: data.type || 'text',
					name: data.name,
					value: data.value,
					required: true,
					placeholder: data.hint || data.text
				};
			}
		);

		/**
		 * Создать Ply-слой на основе фабрики
		 * @param   {String}  name       название фабрики
		 * @param   {Object}  [options]  опции
		 * @param   {Object}  [data]     данные для фабрики
		 * @returns {Promise}
		 */
		Ply.create = function (name, options, data) {
			if (_isNode( name )) {
				return _resolvePromise( new Ply( _extend( options || {}, {el: name} ) ) );
			}

			if ( ! data) {
				data    = options;
				options = {};
			}

			var renderer = (factory[name] || factory['default']);
			return renderer( options, data ).then(
				function (el) {
					return new Ply( _extend( options, {el: el} ) );
				}
			);
		};

		/**
		 * Открыть Ply-слой
		 * @param   {String}  name
		 * @param   {Object}  [options]
		 * @param   {Object}  [data]
		 * @returns {Promise}
		 */
		Ply.open = function (name, options, data) {
			return Ply.create( name, options, data ).then(
				function (layer) {
					return layer.open();
				}
			);
		};

		/**
		 * Создать диалог или систему диалогов
		 * @param   {String|Object}  name
		 * @param   {Object}         [options]
		 * @param   {Object}         [data]
		 * @returns {Promise}
		 */
		Ply.dialog = function (name, options, data) {
			if ((name instanceof Object) && ! _isNode( name )) {
				options = options || /* istanbul ignore next */ {};

				return _promise(
					function (resolve, reject) {
						var first = options.initState,
						current,
						rootLayer,
						interactive,
						stack     = name,
						dialogs   = {},

						_progress = function (ui, layer) {
							(options.progress || /* istanbul ignore next */ noop)(
								_extend(
									{
										name: current.$name,
										index: current.$index,
										length: length,
										stack: stack,
										current: current,
										widget: layer
									},
									ui
								),
								dialogs
							);
						},

						changeLayer = function (spec, effect, callback) {
							// Клонирование данных
							var data = JSON.parse( JSON.stringify( spec.data ) );

							current     = spec; // текущий диалог
							interactive = true; // идет анимация
							(spec.prepare || noop)( data, dialogs );

							Ply.create( spec.ui || 'alert', spec.options || {}, data ).then(
								function (layer) {
									var promise;

									if (rootLayer) {
										promise = rootLayer[/^inner/.test( effect ) ? 'innerSwap' : 'swap']( layer, effect );
									} else {
										rootLayer = layer;
										promise   = rootLayer.open();
									}

									promise.then(
										function () {
											dialogs[spec.$name].el = rootLayer.layerEl;
											interactive            = false;
										}
									);

									callback( rootLayer );
								}
							);
						};

						var length = 0;
						_each(
							stack,
							function (spec, key) {
								first        = first || key;
								spec.effects = spec.effects || {};
								spec.$name   = key;
								spec.$index  = length++;
								dialogs[key] = new Ply.Context();
							}
						);
						stack.$length = length;

						changeLayer(
							stack[first],
							null,
							function (layer) {
								_progress( {}, layer );

								//noinspection FunctionWithInconsistentReturnsJS
								rootLayer.options.onaction = function (ui) {
									if (interactive) {
										return false;
									}

									var isNext = ui.state || (current.back === 'next'),
									swap       = stack[current[isNext ? 'next' : 'back']];

									//                      console.log(current.$name, stack[current[isNext ? 'next' : 'back']]);

									if (swap) {
										changeLayer(
											swap,
											current[isNext ? 'nextEffect' : 'backEffect'],
											function (layer) {
												_progress( ui, layer );
											}
										);

										return false;
									} else {
										(ui.state ? resolve : /* istanbul ignore next */ reject)( ui, dialogs );
									}
								};
							}
						);
					}
				);
			} else {
				if ( ! data && ! _isNode( name )) {
					data    = options || {};
					options = {};
				}

				return Ply.open( name, options, data ).then(
					function (layer) {
						return _promise(
							function (resolve, reject) {
								layer.options.onclose = function () {
									(layer.result.state ? resolve : reject)( layer.result );
								};
							}
						);
					}
				);
			}
		};

		// Export
		Ply.ui      = ui;
		Ply.factory = factory;

		//
		// Export
		//
		Ply.lang     = lang;
		Ply.css      = _css;
		Ply.cssHooks = _cssHooks;

		Ply.keys     = keys;
		Ply.noop     = noop;
		Ply.each     = _each;
		Ply.extend   = _extend;
		Ply.promise  = _promise;
		Ply.Promise  = Promise;
		Ply.support  = support;
		Ply.defaults = _defaults;
		Ply.attrName = _plyAttr;
		Ply.Context  = Context;

		Ply.dom = {
			build: _buildDOM,
			append: _appendChild,
			remove: _removeElement,
			addEvent: _addEvent,
			removeEvent: _removeEvent
		};

		Ply.version = '0.6.1';

		return Ply;
	}
);
