// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

Ext.define('Shopware.apps.ViisonPickwareMobilePickProfiles.controller.Edit', {

    extend: 'Ext.app.Controller',
    mixins: [
        'Shopware.apps.ViisonCommonApp.Mixin',
    ],
    viisonSnippetNamespace: 'backend/viison_pickware_mobile_pick_profiles/main',

    /**
     * A counter used to control when layout updates need to be resumed again. You must not ever manipulate this
     * property directly!
     *
     * @type {number}
     */
    layoutUpdateSuspensionCount: 0,

    /**
     * @override
     */
    init: function () {
        this.control({
            'viison_pickware_mobile_pick_profiles-edit button[action=cancel]': {
                click: this.onCancel,
            },
            'viison_pickware_mobile_pick_profiles-edit button[action=save]': {
                click: this.onSave,
            },
            'viison_pickware_mobile_pick_profiles-edit-order_filter_settings combobox': {
                change: this.onOrderFilterSettingsChange,
            },
            'viison_pickware_mobile_pick_profiles-edit-order_filter_settings numberfield': {
                change: this.onOrderFilterSettingsChange,
            },
            'viison_pickware_mobile_pick_profiles-edit-order_filter_rules': {
                extendRule: this.onRootExtendRule,
            },
            'viison_pickware_mobile_pick_profiles-edit-order_filter_rule_editor': {
                extendRule: this.onExtendRule,
                removeRule: this.onRemoveRule,
            },
            'viison_pickware_mobile_pick_profiles-edit-order_filter_rule_editor viison_pickware_mobile_pick_profiles-edit-rule_form': {
                rightOperandChanged: this.onRuleEditorRightOperandChanged,
            },
            // We cannot narrow the matched events because the combo boxes might fire before they are added to the
            // component hierarchie of this app
            'viison_common_combo_box-combo_box': {
                referenceChanged: this.onRuleEditorReferenceChanged,
                groupingConstraintChanged: this.onRuleEditorGroupingConstraintChanged,
                leftOperandChanged: this.onRuleEditorLeftOperandChanged,
                operatorChanged: this.onRuleEditorOperatorChanged,
            },
        });

        this.callParent(arguments);
    },

    /**
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.model.PickProfile} pickProfile
     * @return {Shopware.apps.ViisonPickwareMobilePickProfiles.view.Edit}
     */
    createEditWindow: function (pickProfile) {
        var windowTitle = (pickProfile.get('name').length > 0) ? (this.getViisonSnippet('edit/window_title/edit') + ' - ' + pickProfile.get('name')) : this.getViisonSnippet('edit/window_title/add');
        var editWindow = this.getView('Edit').create({
            title: windowTitle,
            pickProfile: pickProfile,
        });

        return editWindow;
    },

    /**
     * @param {Ext.button.Button} button
     */
    onCancel: function (button) {
        button.up('viison_pickware_mobile_pick_profiles-edit').close();
    },

    /**
     * Saves the edited pick profile.
     *
     * @param {Ext.button.Button} button
     */
    onSave: function (button) {
        var editWindow = button.up('viison_pickware_mobile_pick_profiles-edit');

        // Validate the settings forms
        var settingsForm = editWindow.down('viison_pickware_mobile_pick_profiles-edit-settings');
        var orderFilterSettingsForm = editWindow.down('viison_pickware_mobile_pick_profiles-edit-order_filter_settings');
        var rootRuleEditor = editWindow
            .down('viison_pickware_mobile_pick_profiles-edit-order_filter_rules')
            .down('viison_pickware_mobile_pick_profiles-edit-order_filter_rule_editor');
        if (!settingsForm.getForm().isValid() || !orderFilterSettingsForm.getForm().isValid() || !rootRuleEditor.isValid()) {
            Shopware.Notification.createGrowlMessage(
                this.getViisonSnippet('notification/error/title'),
                this.getViisonSnippet('notification/error/message/pick_profile_data_invalid'),
                'ViisonPickwareMobilePickProfiles'
            );

            return;
        }

        // Update the edited pick profile with the form and rule data
        var pickProfile = editWindow.pickProfile;
        settingsForm.getForm().updateRecord(pickProfile);
        orderFilterSettingsForm.getForm().updateRecord(pickProfile);
        pickProfile.set('orderFilterQueryConditions', rootRuleEditor.ruleDescription);

        // Sync the changes to the server
        if (pickProfile.phantom) {
            // Add the new pick profile to the store first
            this.getController('Main').pickProfileStore.add(pickProfile);
        }
        this.getController('Main').syncPickProfiles(
            this.getViisonSnippet('notification/error/message/save_pick_profile'),
            function (success) {
                if (success) {
                    Shopware.Notification.createGrowlMessage(
                        this.getViisonSnippet('notification/success/title/pick_profile_saved'),
                        Ext.String.format(
                            this.getViisonSnippet('notification/success/message/pick_profile_saved'),
                            pickProfile.get('name')
                        ),
                        'ViisonPickwareMobilePickProfiles'
                    );
                    editWindow.close();
                }
            }.bind(this)
        );
    },

    /**
     * @param {Ext.form.Field} field
     */
    onOrderFilterSettingsChange: function (field) {
        this.updateOrderFilterEstimates(field.up('viison_pickware_mobile_pick_profiles-edit'));
    },

    /**
     * Creates a new top level editor at the bottom of the editor list.
     *
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRules} orderFilterRulesPanel
     */
    onRootExtendRule: function (orderFilterRulesPanel) {
        this.onExtendRule(orderFilterRulesPanel.rootEditor);

        // Scroll to the bottom of the container
        var domNode = orderFilterRulesPanel.body.dom;
        domNode.scrollTop = domNode.scrollHeight - domNode.offsetHeight;
    },

    /**
     * Extends the given rule of the given `ruleEditor` by a new child rule. If the extended rule is no a composition
     * rule yet, it is converted into one, using the old rule as the first child rule of the composition and the new
     * rule as the second child rule.
     *
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRuleEditor} ruleEditor
     */
    onExtendRule: function (ruleEditor) {
        if (!ruleEditor.canAddChildRules()) {
            throw new Error('Cannot convert the editor\'s rule to a composition rule, because the max. nesting level would be exceeded.');
        }

        // Suspend layout updates while updating the rule tree
        this.suspendLayoutUpdates();

        if (!ruleEditor.isCompositionRuleEditor()) {
            // Convert this editor's rule description into a composition rule
            var booleanOperator = (ruleEditor.parentEditor && ruleEditor.parentEditor.ruleDescription.booleanOperator === 'AND') ? 'OR' : 'AND';
            var originalRuleDescription = ruleEditor.ruleDescription;
            ruleEditor.setRuleDescription({
                type: 'booleanComposition',
                booleanOperator: booleanOperator,
                compositionComponents: [],
            });

            // Reset the internal state of the editor's rule form. This is necessary to get a correct configuration once
            // the rule editor becomes an editable component again (i.e. not a composition editor).
            ruleEditor.ruleForm.resetRightOperandField();

            if (originalRuleDescription) {
                // Add the editor's original rule description to the same editor, which is now a composition
                ruleEditor.addChildRule(originalRuleDescription);
            }
        }

        // Add a new child rule component
        var pickProfile = ruleEditor.up('viison_pickware_mobile_pick_profiles-edit-order_filter_rules').pickProfile;
        ruleEditor.addChildRule(pickProfile.self.createDefaultRuleDescription());

        this.updateOrderFilterEstimates(ruleEditor.up('viison_pickware_mobile_pick_profiles-edit'));

        // Resume layout updates to reflect rule changes in the UI
        this.resumeLayoutUpdates();
    },

    /**
     * Removes the rule of the the given `ruleEditor` from its parent. Finally the remaining rule is simplified by
     * reducing nesting of composition rules that contain only a single child rule.
     *
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRuleEditor} ruleEditor
     */
    onRemoveRule: function (ruleEditor) {
        var parentEditor = ruleEditor.parentEditor;
        if (!parentEditor || ruleEditor.getRuleIndex() === -1) {
            return;
        }

        // Suspend layout updates while updating the rule tree
        this.suspendLayoutUpdates();

        var editWindow = ruleEditor.up('viison_pickware_mobile_pick_profiles-edit');

        parentEditor.removeChildRule(ruleEditor.ruleDescription);
        this.reduceRuleEditorNesting(ruleEditor);

        // Reducing the nesting might leave an empty parent editor behind, which must be removed
        if (parentEditor.isCompositionRuleEditor() && !parentEditor.hasChildRuleDescriptions()) {
            // We must not remove the root editor, but instead need to clear its rule description
            if (parentEditor.isRootEditor()) {
                parentEditor.setRuleDescription(null);
            } else {
                this.onRemoveRule(parentEditor);
            }
        }

        this.updateOrderFilterEstimates(editWindow);

        // Resume layout updates to reflect rule changes in the UI
        this.resumeLayoutUpdates();
    },

    /**
     * Recursively reduces the nesting of the given `ruleEditor`. As long as the parent editor contains only the given
     * `ruleEditor`, its rule is moved into the parent and the given `ruleEditor` is removed.
     *
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRuleEditor} ruleEditor
     */
    reduceRuleEditorNesting: function (ruleEditor) {
        var parentEditor = ruleEditor.parentEditor;
        if (!parentEditor || parentEditor.isRootEditor() || parentEditor.getChildEditors().length !== 1 || parentEditor.getChildEditors()[0].hasChildRuleDescriptions()) {
            return;
        }

        // Integrate the only child rule into its parent
        var lastChildRuleDescription = parentEditor.getChildRuleDescriptions()[0];
        parentEditor.removeChildRule(lastChildRuleDescription);
        parentEditor.setRuleDescription(lastChildRuleDescription);

        // Keep reducing recursively
        this.reduceRuleEditorNesting(parentEditor);
    },

    /**
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.model.rule.Reference} reference
     * @param {Shopware.apps.ViisonCommonComboBox.view.ComboBox} comboBox
     */
    onRuleEditorReferenceChanged: function (reference, comboBox) {
        if (!reference) {
            return;
        }

        // Suspend layout updates while updating the rule tree
        this.suspendLayoutUpdates();

        var ruleForm = comboBox.up('viison_pickware_mobile_pick_profiles-edit-rule_form');
        ruleForm.ruleDescription.leftOperand.tableName = reference.get('id');

        // Update the grouping constraint field
        if (reference.get('groupingConstraints')) {
            ruleForm.groupingConstraintComboBox.initialValue = ruleForm.ruleDescription.groupingConstraint;
            ruleForm.groupingConstraintComboBox.store.loadData(reference.get('groupingConstraints'));
            ruleForm.groupingConstraintComboBox.show();
        } else {
            delete ruleForm.ruleDescription.groupingConstraint;
            ruleForm.groupingConstraintComboBox.hide();
        }

        // Update the left operand field
        ruleForm.leftOperandComboBox.setValue(null);
        ruleForm.leftOperandComboBox.initialValue = ruleForm.ruleDescription.leftOperand.fieldName;
        ruleForm.leftOperandComboBox.store.loadData(reference.get('fields'));

        // Reset the right operand, if the rule form is not being changed because a rule description was set, e.g.
        // because a user interaction triggered the reference change
        if (!ruleForm.isSettingRuleDescription && ruleForm.getRightOperandField()) {
            ruleForm.ruleDescription.rightOperand.value = (ruleForm.getRightOperandField().operandValueType === 'text') ? '' : null;
            ruleForm.updateRightOperandField();
        }

        this.updateOrderFilterEstimates(ruleForm.up('viison_pickware_mobile_pick_profiles-edit'));

        // Resume layout updates to reflect rule changes in the UI
        this.resumeLayoutUpdates();
    },

    /**
     * @param {Shopware.data.Model} groupingConstraint
     * @param {Shopware.apps.ViisonCommonComboBox.view.ComboBox} comboBox
     */
    onRuleEditorGroupingConstraintChanged: function (groupingConstraint, comboBox) {
        var ruleForm = comboBox.up('viison_pickware_mobile_pick_profiles-edit-rule_form');
        ruleForm.ruleDescription.groupingConstraint = (groupingConstraint) ? groupingConstraint.get('id') : null;

        this.updateOrderFilterEstimates(ruleForm.up('viison_pickware_mobile_pick_profiles-edit'));
    },

    /**
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.model.rule.Field} leftOperand
     * @param {Shopware.apps.ViisonCommonComboBox.view.ComboBox} comboBox
     */
    onRuleEditorLeftOperandChanged: function (leftOperand, comboBox, oldValue) {
        if (!leftOperand) {
            return;
        }

        // Suspend layout updates while updating the rule tree
        this.suspendLayoutUpdates();

        var ruleForm = comboBox.up('viison_pickware_mobile_pick_profiles-edit-rule_form');
        ruleForm.ruleDescription.leftOperand.fieldName = leftOperand.get('id');
        var leftOperandValueType = leftOperand.get('valueType');

        // If the type of the currently visible right operand field and the new left operand do not match, we
        // need to reset the rule descriptions right operand to a default value.
        var currentRightOperandField = ruleForm.getRightOperandField();
        var oldLeftOperand = comboBox.findRecordByValue(oldValue);
        var oldValueType = (oldLeftOperand) ? oldLeftOperand.getEffectiveValueType() : null;
        if (oldValueType !== leftOperand.getEffectiveValueType() && currentRightOperandField) {
            var currentRightOperandFieldType = currentRightOperandField.operandValueType;
            if (currentRightOperandFieldType === 'store') {
                currentRightOperandFieldType = Ext.getClass(currentRightOperandField.items.first().store);
            }
            if (leftOperand.getEffectiveValueType() !== currentRightOperandFieldType) {
                ruleForm.ruleDescription.rightOperand.value = (leftOperandValueType === 'text') ? '' : null;
            }
        }

        // Update the operator field. Since its store will always be the same and we only filter it based on the
        // supported value types of each operator, we need to clear the filter first. Then we filter it again using the
        // value type as well as the nullability of our new left operand. Note: In order to pre-select the correct
        // value, i.e. the operator set in the form's rule description, we need to reset the comboboxes value (without
        // using the setter) and set its `initialValue` to the correct operator. This works around the "statefulness" of
        // the combo box, which preserves its current value, if it is still available in its store, even though a new
        // `initialValue` was set.
        ruleForm.operatorComboBox.store.clearFilter(true);
        ruleForm.operatorComboBox.setValue(null);
        ruleForm.operatorComboBox.initialValue = ruleForm.ruleDescription.operator;
        // Note: Filtering the operator combobox store eventually leads to `onRuleEditorOperatorChanged()` being called,
        // which in turn will update the right operand field to show the new value as changed above.
        ruleForm.operatorComboBox.store.filter([
            {
                filterFn: function (operator) {
                    return (
                        operator.supportsValueOfType(leftOperandValueType)
                        && (!operator.get('requiresNullability') || leftOperand.get('isNullable'))
                    );
                },
            },
        ]);

        this.updateOrderFilterEstimates(ruleForm.up('viison_pickware_mobile_pick_profiles-edit'));

        // Resume layout updates to reflect rule changes in the UI
        this.resumeLayoutUpdates();
    },

    /**
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.model.rule.Operator} operator
     * @param {Shopware.apps.ViisonCommonComboBox.view.ComboBox} comboBox
     */
    onRuleEditorOperatorChanged: function (operator, comboBox) {
        if (!operator) {
            return;
        }

        // Suspend layout updates while updating the rule tree
        this.suspendLayoutUpdates();

        var ruleForm = comboBox.up('viison_pickware_mobile_pick_profiles-edit-rule_form');
        ruleForm.ruleDescription.operator = operator.get('value');
        ruleForm.ruleDescription.type = operator.get('type');
        ruleForm.updateRightOperandField();

        this.updateOrderFilterEstimates(ruleForm.up('viison_pickware_mobile_pick_profiles-edit'));

        // Resume layout updates to reflect rule changes in the UI
        this.resumeLayoutUpdates();
    },

    /**
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.model.rule.Field} rightOperand
     * @param {Ext.form.Field} field
     */
    onRuleEditorRightOperandChanged: function (rightOperand, field) {
        // Suspend layout updates while updating the rule tree
        this.suspendLayoutUpdates();

        var ruleForm = field.up('viison_pickware_mobile_pick_profiles-edit-rule_form');
        ruleForm.ruleDescription.rightOperand.value = rightOperand;

        this.updateOrderFilterEstimates(ruleForm.up('viison_pickware_mobile_pick_profiles-edit'));

        // Resume layout updates to reflect rule changes in the UI
        this.resumeLayoutUpdates();
    },

    /**
     * Validates both the filter settings form and all rule editor forms and, if valid, evaluates the picking order
     * filter using the current settings and rules to display both the time it took to run the filter and the current
     * number of results in the `editWindow`'s toolbar.
     *
     * Note: Calls to this method are buffered for 3 seconds. That is, the first call to this method is delayed by 3
     * seconds and any subsequent calls within that timespan are ignored. This reduces the load on the server, because
     * running the picking order filter can be quite resource intensive.
     *
     * @param {Shopware.apps.ViisonPickwareMobilePickProfiles.view.Edit} editWindow
     */
    updateOrderFilterEstimates: function (editWindow) {
        if (!editWindow) {
            return;
        }

        var orderFilterPreview = editWindow.down('viison_pickware_mobile_pick_profiles-edit-order_filter_preview');
        orderFilterPreview.setLoading(true);

        this.bufferedOrderFilterEstimateUpdate(editWindow);
    },

    bufferedOrderFilterEstimateUpdate: Ext.Function.createBuffered(
        function (editWindow) {
            // Validate the order filter settings form and all rule editors
            var orderFilterSettingsForm = editWindow.down(
                'viison_pickware_mobile_pick_profiles-edit-order_filter_settings'
            );
            var filterRulesPanel = editWindow.down('viison_pickware_mobile_pick_profiles-edit-order_filter_rules');
            // Make sure the settings form and rules panel still exist. This is necessary because calls to this function
            // are buffered and might be executed after the edit window was closed.
            if (!orderFilterSettingsForm || !filterRulesPanel) {
                return;
            }
            var rootRuleEditor = filterRulesPanel.down(
                'viison_pickware_mobile_pick_profiles-edit-order_filter_rule_editor'
            );
            var orderFilterPreview = editWindow.down('viison_pickware_mobile_pick_profiles-edit-order_filter_preview');
            if (!orderFilterSettingsForm.getForm().isValid() || !rootRuleEditor.isValid()) {
                orderFilterPreview.displayErrorMessage(null);

                return;
            }

            // Collect the filter params
            var filterSettings = orderFilterSettingsForm.getForm().getValues();
            var filterParams = {
                stockBasedOrderFilterMode: filterSettings.stockBasedOrderFilterMode,
                stockBasedOrderFilterExemptDispatchMethodIds: filterSettings.stockBasedOrderFilterExemptDispatchMethods,
                advanceDaysForPreOrderedItems: filterSettings.advanceDaysForPreOrderedItems,
                orderFilterQueryConditions: rootRuleEditor.ruleDescription,
            };

            // Execute the order filter
            Ext.Ajax.request({
                url: ViisonCommonApp.assembleBackendUrl('ViisonPickwareMobilePickProfiles/testOrderFilter'),
                method: 'POST',
                jsonData: filterParams,
                scope: this,
                success: function (response) {
                    orderFilterPreview.setLoading(false);
                    var responseData = Ext.JSON.decode(response.responseText, true);
                    if (responseData && responseData.data) {
                        orderFilterPreview.updateFilterEstimateLabels(
                            responseData.data.filterExecutionTime,
                            responseData.data.filterResultCount
                        );
                    } else {
                        orderFilterPreview.displayErrorMessage((responseData) ? responseData.message : null);
                    }
                },
                failure: function () {
                    orderFilterPreview.setLoading(false);
                    orderFilterPreview.displayErrorMessage();
                },
            });
        },
        3000
    ),

    /**
     * Suspends all layout updates of ExtJS. Use this method instead of calling `Ext.suspendLayouts()` directly to
     * ensure nested suspension of layout updates works correctly.
     */
    suspendLayoutUpdates: function () {
        this.layoutUpdateSuspensionCount += 1;
        if (this.layoutUpdateSuspensionCount === 1) {
            Ext.suspendLayouts();
        }
    },

    /**
     * Resumes all layout updates of ExtJS, if calling this method reduces `layoutUpdateSuspensionCount` to zero. Use
     * this method instead of calling `Ext.suspendLayouts()` directly to ensure nested suspension of layout updates
     * works correctly.
     */
    resumeLayoutUpdates: function () {
        this.layoutUpdateSuspensionCount -= 1;
        if (this.layoutUpdateSuspensionCount === 0) {
            Ext.resumeLayouts(true);
        }
    },
});
