/**
 * DevExtreme (integration/angular/component_registrator.js)
 * Version: 17.2.10 (build 18271)
 * Build date: Fri Sep 28 2018
 *
 * Copyright (c) 2012 - 2018 Developer Express Inc. ALL RIGHTS RESERVED
 * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
 */
"use strict";
var $ = require("../../core/renderer"),
    eventsEngine = require("../../events/core/events_engine"),
    Config = require("../../core/config"),
    registerComponentCallbacks = require("../../core/component_registrator_callbacks"),
    Class = require("../../core/class"),
    Callbacks = require("../../core/utils/callbacks"),
    typeUtils = require("../../core/utils/type"),
    each = require("../../core/utils/iterator").each,
    inArray = require("../../core/utils/array").inArray,
    Locker = require("../../core/utils/locker"),
    Widget = require("../../ui/widget/ui.widget"),
    Editor = require("../../ui/editor/editor"),
    NgTemplate = require("./template"),
    ngModule = require("./module"),
    CollectionWidget = require("../../ui/collection/ui.collection_widget.edit"),
    compileSetter = require("../../core/utils/data").compileSetter,
    compileGetter = require("../../core/utils/data").compileGetter,
    extendFromObject = require("../../core/utils/extend").extendFromObject,
    inflector = require("../../core/utils/inflector"),
    errors = require("../../core/errors");
var ITEM_ALIAS_ATTRIBUTE_NAME = "dxItemAlias",
    SKIP_APPLY_ACTION_CATEGORIES = ["rendering"],
    NG_MODEL_OPTION = "value";
var safeApply = function(func, scope) {
    if (scope.$root.$$phase) {
        return func(scope)
    } else {
        return scope.$apply(function() {
            return func(scope)
        })
    }
};
var ComponentBuilder = Class.inherit({
    ctor: function(options) {
        this._componentDisposing = Callbacks();
        this._optionChangedCallbacks = Callbacks();
        this._ngLocker = new Locker;
        this._scope = options.scope;
        this._$element = options.$element;
        this._$templates = options.$templates;
        this._componentClass = options.componentClass;
        this._parse = options.parse;
        this._compile = options.compile;
        this._itemAlias = options.itemAlias;
        this._transcludeFn = options.transcludeFn;
        this._digestCallbacks = options.dxDigestCallbacks;
        this._normalizeOptions(options.ngOptions);
        this._initComponentBindings();
        this._initComponent(this._scope);
        if (!options.ngOptions) {
            this._addOptionsStringWatcher(options.ngOptionsString)
        }
    },
    _addOptionsStringWatcher: function(optionsString) {
        var that = this;
        var clearOptionsStringWatcher = that._scope.$watch(optionsString, function(newOptions) {
            if (!newOptions) {
                return
            }
            clearOptionsStringWatcher();
            that._normalizeOptions(newOptions);
            that._initComponentBindings();
            that._component.option(that._evalOptions(that._scope))
        });
        that._componentDisposing.add(clearOptionsStringWatcher)
    },
    _normalizeOptions: function(options) {
        var that = this;
        that._ngOptions = extendFromObject({}, options);
        if (!options) {
            return
        }
        if (!options.hasOwnProperty("bindingOptions") && options.bindingOptions) {
            that._ngOptions.bindingOptions = options.bindingOptions
        }
        if (options.bindingOptions) {
            each(options.bindingOptions, function(key, value) {
                if ("string" === typeUtils.type(value)) {
                    that._ngOptions.bindingOptions[key] = {
                        dataPath: value
                    }
                }
            })
        }
    },
    _initComponent: function(scope) {
        this._component = new this._componentClass(this._$element, this._evalOptions(scope));
        this._component._isHidden = true;
        this._handleDigestPhase()
    },
    _handleDigestPhase: function() {
        var that = this,
            beginUpdate = function() {
                that._component.beginUpdate()
            },
            endUpdate = function() {
                that._component.endUpdate()
            };
        that._digestCallbacks.begin.add(beginUpdate);
        that._digestCallbacks.end.add(endUpdate);
        that._componentDisposing.add(function() {
            that._digestCallbacks.begin.remove(beginUpdate);
            that._digestCallbacks.end.remove(endUpdate)
        })
    },
    _initComponentBindings: function() {
        var that = this,
            optionDependencies = {};
        if (!that._ngOptions.bindingOptions) {
            return
        }
        each(that._ngOptions.bindingOptions, function(optionPath, value) {
            var prevWatchMethod, clearWatcher, separatorIndex = optionPath.search(/\[|\./),
                optionForSubscribe = separatorIndex > -1 ? optionPath.substring(0, separatorIndex) : optionPath,
                valuePath = value.dataPath,
                deepWatch = true,
                forcePlainWatchMethod = false;
            if (void 0 !== value.deep) {
                forcePlainWatchMethod = deepWatch = !!value.deep
            }
            if (!optionDependencies[optionForSubscribe]) {
                optionDependencies[optionForSubscribe] = {}
            }
            optionDependencies[optionForSubscribe][optionPath] = valuePath;
            var watchCallback = function(newValue, oldValue) {
                if (that._ngLocker.locked(optionPath)) {
                    return
                }
                that._ngLocker.obtain(optionPath);
                that._component.option(optionPath, newValue);
                updateWatcher();
                if (that._component._optionValuesEqual(optionPath, oldValue, newValue) && that._ngLocker.locked(optionPath)) {
                    that._ngLocker.release(optionPath)
                }
            };
            var updateWatcher = function() {
                var watchMethod = Array.isArray(that._scope.$eval(valuePath)) && !forcePlainWatchMethod ? "$watchCollection" : "$watch";
                if (prevWatchMethod !== watchMethod) {
                    if (clearWatcher) {
                        clearWatcher()
                    }
                    clearWatcher = that._scope[watchMethod](valuePath, watchCallback, deepWatch);
                    prevWatchMethod = watchMethod
                }
            };
            updateWatcher();
            that._componentDisposing.add(clearWatcher)
        });
        that._optionChangedCallbacks.add(function(args) {
            var optionName = args.name,
                fullName = args.fullName,
                component = args.component;
            if (that._ngLocker.locked(fullName)) {
                that._ngLocker.release(fullName);
                return
            }
            if (!optionDependencies || !optionDependencies[optionName]) {
                return
            }
            var isActivePhase = that._scope.$root.$$phase;
            var obtainOption = function() {
                that._ngLocker.obtain(fullName)
            };
            if (isActivePhase) {
                that._digestCallbacks.begin.add(obtainOption)
            } else {
                obtainOption()
            }
            safeApply(function() {
                each(optionDependencies[optionName], function(optionPath, valuePath) {
                    if (!that._optionsAreLinked(fullName, optionPath)) {
                        return
                    }
                    var value = component.option(optionPath);
                    that._parse(valuePath).assign(that._scope, value);
                    var scopeValue = that._parse(valuePath)(that._scope);
                    if (scopeValue !== value) {
                        args.component.option(optionPath, scopeValue)
                    }
                })
            }, that._scope);
            var releaseOption = function() {
                if (that._ngLocker.locked(fullName)) {
                    that._ngLocker.release(fullName)
                }
                that._digestCallbacks.begin.remove(obtainOption);
                that._digestCallbacks.end.remove(releaseOption)
            };
            if (isActivePhase) {
                that._digestCallbacks.end.addPrioritized(releaseOption)
            } else {
                releaseOption()
            }
        })
    },
    _optionsAreNested: function(optionPath1, optionPath2) {
        var parentSeparator = optionPath1[optionPath2.length];
        return 0 === optionPath1.indexOf(optionPath2) && ("." === parentSeparator || "[" === parentSeparator)
    },
    _optionsAreLinked: function(optionPath1, optionPath2) {
        if (optionPath1 === optionPath2) {
            return true
        }
        return optionPath1.length > optionPath2.length ? this._optionsAreNested(optionPath1, optionPath2) : this._optionsAreNested(optionPath2, optionPath1)
    },
    _compilerByTemplate: function(template) {
        var that = this,
            scopeItemsPath = this._getScopeItemsPath();
        return function(options) {
            var $resultMarkup = $(template).clone(),
                dataIsScope = options.model && options.model.constructor === that._scope.$root.constructor,
                templateScope = dataIsScope ? options.model : options.noModel ? that._scope : that._createScopeWithData(options);
            if (scopeItemsPath) {
                that._synchronizeScopes(templateScope, scopeItemsPath, options.index)
            }
            $resultMarkup.appendTo(options.container);
            if (!options.noModel) {
                eventsEngine.on($resultMarkup, "$destroy", function() {
                    var destroyAlreadyCalled = !templateScope.$parent;
                    if (destroyAlreadyCalled) {
                        return
                    }
                    templateScope.$destroy()
                })
            }
            that._applyAsync(that._compile($resultMarkup, that._transcludeFn), templateScope);
            return $resultMarkup
        }
    },
    _applyAsync: function(func, scope) {
        var that = this;
        func(scope);
        if (!scope.$root.$$phase) {
            if (!that._renderingTimer) {
                that._renderingTimer = setTimeout(function() {
                    scope.$apply();
                    that._renderingTimer = null
                })
            }
            that._componentDisposing.add(function() {
                clearTimeout(that._renderingTimer)
            })
        }
    },
    _getScopeItemsPath: function() {
        if (this._componentClass.subclassOf(CollectionWidget) && this._ngOptions.bindingOptions && this._ngOptions.bindingOptions.items) {
            return this._ngOptions.bindingOptions.items.dataPath
        }
    },
    _createScopeWithData: function(options) {
        var newScope = this._scope.$new();
        if (this._itemAlias) {
            newScope[this._itemAlias] = options.model
        }
        if (typeUtils.isDefined(options.index)) {
            newScope.$index = options.index
        }
        return newScope
    },
    _synchronizeScopes: function(itemScope, parentPrefix, itemIndex) {
        if (this._itemAlias && "object" !== typeof itemScope[this._itemAlias]) {
            this._synchronizeScopeField({
                parentScope: this._scope,
                childScope: itemScope,
                fieldPath: this._itemAlias,
                parentPrefix: parentPrefix,
                itemIndex: itemIndex
            })
        }
    },
    _synchronizeScopeField: function(args) {
        var parentScope = args.parentScope,
            childScope = args.childScope,
            fieldPath = args.fieldPath,
            parentPrefix = args.parentPrefix,
            itemIndex = args.itemIndex;
        var optionOuterPath, innerPathSuffix = fieldPath === this._itemAlias ? "" : "." + fieldPath,
            collectionField = void 0 !== itemIndex,
            optionOuterBag = [parentPrefix];
        if (collectionField) {
            if (!typeUtils.isNumeric(itemIndex)) {
                return
            }
            optionOuterBag.push("[", itemIndex, "]")
        }
        optionOuterBag.push(innerPathSuffix);
        optionOuterPath = optionOuterBag.join("");
        var clearParentWatcher = parentScope.$watch(optionOuterPath, function(newValue, oldValue) {
            if (newValue !== oldValue) {
                compileSetter(fieldPath)(childScope, newValue)
            }
        });
        var clearItemWatcher = childScope.$watch(fieldPath, function(newValue, oldValue) {
            if (newValue !== oldValue) {
                if (collectionField && !compileGetter(parentPrefix)(parentScope)[itemIndex]) {
                    clearItemWatcher();
                    return
                }
                compileSetter(optionOuterPath)(parentScope, newValue)
            }
        });
        this._componentDisposing.add([clearParentWatcher, clearItemWatcher])
    },
    _evalOptions: function(scope) {
        var result = extendFromObject({}, this._ngOptions);
        delete result.bindingOptions;
        if (this._ngOptions.bindingOptions) {
            each(this._ngOptions.bindingOptions, function(key, value) {
                result[key] = scope.$eval(value.dataPath)
            })
        }
        result._optionChangedCallbacks = this._optionChangedCallbacks;
        result._disposingCallbacks = this._componentDisposing;
        result.onActionCreated = function(component, action, config) {
            if (config && inArray(config.category, SKIP_APPLY_ACTION_CATEGORIES) > -1) {
                return action
            }
            var wrappedAction = function() {
                var that = this,
                    args = arguments;
                if (!scope || !scope.$root || scope.$root.$$phase) {
                    return action.apply(that, args)
                }
                return safeApply(function() {
                    return action.apply(that, args)
                }, scope)
            };
            return wrappedAction
        };
        result.beforeActionExecute = result.onActionCreated;
        result.nestedComponentOptions = function(component) {
            return {
                templatesRenderAsynchronously: component.option("templatesRenderAsynchronously"),
                forceApplyBindings: component.option("forceApplyBindings"),
                modelByElement: component.option("modelByElement"),
                onActionCreated: component.option("onActionCreated"),
                beforeActionExecute: component.option("beforeActionExecute"),
                nestedComponentOptions: component.option("nestedComponentOptions")
            }
        };
        result.templatesRenderAsynchronously = true;
        if (Config().wrapActionsBeforeExecute) {
            result.forceApplyBindings = function() {
                safeApply(function() {}, scope)
            }
        }
        result.integrationOptions = {
            createTemplate: function(element) {
                return new NgTemplate(element, this._compilerByTemplate.bind(this))
            }.bind(this),
            watchMethod: function(fn, callback, options) {
                options = options || {};
                var immediateValue;
                var skipCallback = options.skipImmediate;
                var disposeWatcher = scope.$watch(function() {
                    var value = fn();
                    if (value instanceof Date) {
                        value = value.valueOf()
                    }
                    return value
                }, function(newValue) {
                    var isSameValue = immediateValue === newValue;
                    if (!skipCallback && (!isSameValue || isSameValue && options.deep)) {
                        callback(newValue)
                    }
                    skipCallback = false
                }, options.deep);
                if (!skipCallback) {
                    immediateValue = fn();
                    callback(immediateValue)
                }
                if (Config().wrapActionsBeforeExecute) {
                    this._applyAsync(function() {}, scope)
                }
                return disposeWatcher
            }.bind(this),
            templates: {
                "dx-polymorph-widget": {
                    render: function(options) {
                        var widgetName = options.model.widget;
                        if (!widgetName) {
                            return
                        }
                        if ("button" === widgetName || "tabs" === widgetName || "dropDownMenu" === widgetName) {
                            var deprecatedName = widgetName;
                            widgetName = inflector.camelize("dx-" + widgetName);
                            errors.log("W0001", "dxToolbar - 'widget' item field", deprecatedName, "16.1", "Use: '" + widgetName + "' instead")
                        }
                        var markup = $("<div>").attr(inflector.dasherize(widgetName), "options").get(0);
                        var newScope = this._scope.$new();
                        newScope.options = options.model.options;
                        options.container.append(markup);
                        this._compile(markup)(newScope)
                    }.bind(this)
                }
            }
        };
        result.modelByElement = function() {
            return scope
        };
        return result
    }
});
ComponentBuilder = ComponentBuilder.inherit({
    ctor: function(options) {
        this._componentName = options.componentName;
        this._ngModel = options.ngModel;
        this._ngModelController = options.ngModelController;
        this.callBase.apply(this, arguments)
    },
    _isNgModelRequired: function() {
        return this._componentClass.subclassOf(Editor) && this._ngModel
    },
    _initComponentBindings: function() {
        this.callBase.apply(this, arguments);
        this._initNgModelBinding()
    },
    _initNgModelBinding: function() {
        if (!this._isNgModelRequired()) {
            return
        }
        var that = this;
        var clearNgModelWatcher = this._scope.$watch(this._ngModel, function(newValue, oldValue) {
            if (that._ngLocker.locked(NG_MODEL_OPTION)) {
                return
            }
            if (newValue === oldValue) {
                return
            }
            that._component.option(NG_MODEL_OPTION, newValue)
        });
        that._optionChangedCallbacks.add(function(args) {
            that._ngLocker.obtain(NG_MODEL_OPTION);
            try {
                if (args.name !== NG_MODEL_OPTION) {
                    return
                }
                that._ngModelController.$setViewValue(args.value)
            } finally {
                if (that._ngLocker.locked(NG_MODEL_OPTION)) {
                    that._ngLocker.release(NG_MODEL_OPTION)
                }
            }
        });
        this._componentDisposing.add(clearNgModelWatcher)
    },
    _evalOptions: function() {
        if (!this._isNgModelRequired()) {
            return this.callBase.apply(this, arguments)
        }
        var result = this.callBase.apply(this, arguments);
        result[NG_MODEL_OPTION] = this._parse(this._ngModel)(this._scope);
        return result
    }
});
var registeredComponents = {};
var registerComponentDirective = function(name) {
    var priority = "dxValidator" !== name ? 1 : 10;
    ngModule.directive(name, ["$compile", "$parse", "dxDigestCallbacks", function($compile, $parse, dxDigestCallbacks) {
        return {
            restrict: "A",
            require: "^?ngModel",
            priority: priority,
            compile: function($element) {
                var componentClass = registeredComponents[name],
                    $content = componentClass.subclassOf(Widget) ? $element.contents().detach() : null;
                return function(scope, $element, attrs, ngModelController, transcludeFn) {
                    $element.append($content);
                    safeApply(function() {
                        new ComponentBuilder({
                            componentClass: componentClass,
                            componentName: name,
                            compile: $compile,
                            parse: $parse,
                            $element: $element,
                            scope: scope,
                            ngOptionsString: attrs[name],
                            ngOptions: attrs[name] ? scope.$eval(attrs[name]) : {},
                            ngModel: attrs.ngModel,
                            ngModelController: ngModelController,
                            transcludeFn: transcludeFn,
                            itemAlias: attrs[ITEM_ALIAS_ATTRIBUTE_NAME],
                            dxDigestCallbacks: dxDigestCallbacks
                        })
                    }, scope)
                }
            }
        }
    }])
};
registerComponentCallbacks.add(function(name, componentClass) {
    if (!registeredComponents[name]) {
        registerComponentDirective(name)
    }
    registeredComponents[name] = componentClass
});
