// 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.view.edit.OrderFilterRuleEditor', {

    extend: 'Ext.panel.Panel',
    alias: 'widget.viison_pickware_mobile_pick_profiles-edit-order_filter_rule_editor',
    cls: 'viison_pickware_mobile_pick_profiles-edit-order_filter_rule_editor',

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

    layout: {
        type: 'vbox',
        align: 'stretch',
    },

    statics: {
        /**
         * @return {number} The maximum allowed level of rule nesting.
         */
        getMaxAllowedRuleNestingLevel: function () {
            return 3;
        },

        /**
         * @return {Ext.data.Store} A store singleton containing the types of available boolean composition:
         *         'conjunction' and 'disjunction'.
         */
        getBooleanCompositionOperatorStore: function () {
            if (!this.booleanCompositionTypeStore) {
                this.booleanCompositionTypeStore = Ext.create('Ext.data.Store', {
                    fields: [
                        'booleanOperator',
                        'name',
                    ],
                    data: [
                        {
                            booleanOperator: 'AND',
                            name: ViisonCommonApp.getSnippet(
                                'edit/order_filter_rule_editor/conjunction_type_store/type/conjunction',
                                'backend/viison_pickware_mobile_pick_profiles/main'
                            ),
                        },
                        {
                            booleanOperator: 'OR',
                            name: ViisonCommonApp.getSnippet(
                                'edit/order_filter_rule_editor/conjunction_type_store/type/disjunction',
                                'backend/viison_pickware_mobile_pick_profiles/main'
                            ),
                        },
                    ],
                });
            }

            return this.booleanCompositionTypeStore;
        },
    },

    /**
     * @property {Object} ruleDescription
     */
    ruleDescription: undefined,

    /**
     * @property {Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRuleEditor|null} parentEditor
     */
    parentEditor: null,

    /**
     * @override
     */
    initComponent: function () {
        this.ensureRuleDescriptionIsUnique();

        this.items = this.createItems();

        this.callParent(arguments);
    },

    /**
     * Adds the rule with the given `ruleDescription` to the receiving editor. Note: Calling this method updates both
     * the underlying data model and the UI.
     *
     * @param {Object} ruleDescription The description of the rule that should be added to the receiving editor.
     * @throws {Error} If no rules can be added to the receiving editor, because the max. nesting level is
     *         already reached.
     */
    addChildRule: function (ruleDescription) {
        if (!this.canAddChildRules()) {
            throw new Error('Cannot add child rule to editor, because the max. nesting level would be exceeded.');
        }

        // Update the data model first
        this.getChildRuleDescriptions().push(ruleDescription);

        // Add all required components
        var newComponents = [];
        if (this.getChildRuleDescriptions().length > 1) {
            newComponents.push(this.createBooleanCompositionOperatorComboBox());
        }
        newComponents.push(this.createChildRuleEditor(ruleDescription));
        this.contentContainer.add(newComponents);

        // Commit view updates
        this.contentContainer.doLayout();
        this.updateVisibleComponents();

        // Always make the content of the extended rule visible
        this.setContentContainerVisible(true);
    },

    /**
     * Removes the rule with the given `ruleDescription` from the receiving editor. Note: Calling this method updates
     * both the underlying data model and the UI.
     *
     * @param {Object} ruleDescription The description of the rule that should be removed from the receiving editor.
     * @throws {Error} If the passed `ruleDescription` is not a direct child rule of the receiving editor.
     */
    removeChildRule: function (ruleDescription) {
        if (this.getChildRuleDescriptions().indexOf(ruleDescription) === -1) {
            throw new Error('Cannot remove rule from editor because it is not one of its children.');
        }

        // Find the respective rule editor for the given rule description
        var childEditor = this.contentContainer.items.findBy(function (component) {
            return Ext.getClassName(component) === 'Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRuleEditor' && component.ruleDescription === ruleDescription;
        }, this);
        var childEditorIndex = this.contentContainer.items.indexOf(childEditor);
        var childRuleIndex = childEditor.getRuleIndex();

        // Remove all obsolete view components
        if (this.getChildRuleDescriptions().length > 1) {
            if (childRuleIndex === (this.getChildRuleDescriptions().length - 1)) {
                // Last rule removed, hence remove the boolean composition operator combo box right above the removed
                // rule editor
                this.contentContainer.remove(this.contentContainer.items.getAt(childEditorIndex - 1));
            } else {
                // Any other rule removed, hence remove the boolean composition operator combo box right below the
                // removed rule editor
                this.contentContainer.remove(this.contentContainer.items.getAt(childEditorIndex + 1));
            }
        }
        this.contentContainer.remove(childEditor);

        // Update data model
        this.getChildRuleDescriptions().splice(childRuleIndex, 1);

        // Update view
        this.contentContainer.doLayout();
        this.updateVisibleComponents();
        this.updateTooltipsAndLabels();
    },

    /**
     * Changes the rule description of the receiving editor and updates the parent editor's rule description
     * accordingly, if available.
     *
     * @param {Object} ruleDescription The new rule description for the receiving editor.
     */
    setRuleDescription: function (ruleDescription) {
        var ruleDescriptionIndex = this.getRuleIndex();
        this.ruleDescription = ruleDescription;
        this.ensureRuleDescriptionIsUnique();
        if (this.parentEditor && ruleDescriptionIndex > -1) {
            this.parentEditor.getChildRuleDescriptions().splice(ruleDescriptionIndex, 1, ruleDescription);
        }

        this.ruleForm.setRuleDescription(this.ruleDescription);

        this.updateVisibleComponents();
    },

    /**
     * Changes the visibility of the receiving editor's content container and updates the collapse button's
     * icon accordingly.
     *
     * @param {boolean} visible
     */
    setContentContainerVisible: function (visible) {
        var editor = this.contentContainer.up('viison_pickware_mobile_pick_profiles-edit-order_filter_rule_editor');

        if (visible) {
            this.contentContainer.show();
            editor.removeCls('is--collapsed');
            this.collapseButton.setIconCls('is--icon-collapse');
        } else {
            this.contentContainer.hide();
            editor.addCls('is--collapsed');
            this.collapseButton.setIconCls('is--icon-expand');
        }
        this.collapseButton.setTooltip(this.getCollapseButtonTooltip());
        editor.doLayout();
    },

    /**
     * @return {Ext.component.Component[]}
     */
    createItems: function () {
        var items = [
            this.createHeaderContainer(),
            this.createContentContainer(),
        ];

        this.updateVisibleComponents();

        return items;
    },

    /**
     * Updates the visibility of main view components.
     */
    updateVisibleComponents: function () {
        if (this.isRootEditor() || !this.canAddChildRules()) {
            this.headerContainer.hide();
            this.ruleForm.items.last().show();
        } else {
            this.headerContainer.show();
            this.ruleForm.items.last().hide();
        }

        if (!this.ruleDescription || this.isCompositionRuleEditor()) {
            this.ruleForm.hide();
        } else {
            this.ruleForm.show();
        }
    },

    /**
     * Recursively updates the tooltip and label texts of all buttons contained in the receiving editor.
     */
    updateTooltipsAndLabels: function () {
        this.collapseButton.setTooltip(this.getCollapseButtonTooltip());
        this.extendButton.setText(this.getExtendButtonTitle());
        this.removeButtons.forEach(function (removeButton) {
            removeButton.setTooltip(this.getRemoveButtonTooltip());
        }, this);

        // Update the tooltips and labels of all child editors as well
        this.getChildEditors().forEach(function (childEditor) {
            childEditor.updateTooltipsAndLabels();
        }, this);
    },

    /**
     * @return {Ext.container.Container}
     */
    createHeaderContainer: function () {
        this.headerContainer = Ext.create('Ext.container.Container', {
            layout: 'hbox',
            defaults: {
                margin: '0 10px 0 0',
            },
            cls: 'is--rule-header',
            items: [
                this.createCollapseButton(),
                this.createExtendButton(),
                {
                    xtype: 'container',
                    flex: 1,
                },
                this.createRemoveButton(),
            ],
        });

        return this.headerContainer;
    },

    /**
     * @return {Ext.button.Button}
     */
    createCollapseButton: function () {
        this.collapseButton = Ext.create('Ext.button.Button', {
            tooltip: this.getCollapseButtonTooltip(),
            cls: 'is--button-collapse-expand',
            iconCls: 'is--icon-collapse',
            border: false,
            style: {
                background: 'none',
            },
            scope: this,
            handler: function () {
                this.setContentContainerVisible(this.contentContainer.isHidden());
            },
        });

        return this.collapseButton;
    },

    /**
     * @return {string} The tooltip that should be used for the receiving editor's 'collapse' button.
     */
    getCollapseButtonTooltip: function () {
        var snippetName = (this.contentContainer && this.contentContainer.isHidden()) ? 'edit/order_filter_rule_editor/header/button/expand_rule/tooltip' : 'edit/order_filter_rule_editor/header/button/collapse_rule/tooltip';

        return Ext.String.format(this.getViisonSnippet(snippetName), this.getRulePath());
    },

    /**
     * @return {Ext.button.Button}
     */
    createExtendButton: function () {
        this.extendButton = Ext.create('Ext.button.Button', {
            text: this.getExtendButtonTitle(),
            cls: 'secondary small',
            scope: this,
            handler: function () {
                this.fireEvent('extendRule', this);
            },
        });

        return this.extendButton;
    },

    /**
     * @return {string} The tooltip that should be used for the receiving editor's 'extend' button.
     */
    getExtendButtonTitle: function () {
        return Ext.String.format(
            this.getViisonSnippet('edit/order_filter_rule_editor/header/button/extend_rule/title'),
            this.getRulePath()
        );
    },

    /**
     * @return {Ext.button.Button}
     */
    createRemoveButton: function () {
        if (!this.removeButtons) {
            this.removeButtons = [];
        }

        var removeButton = Ext.create('Ext.button.Button', {
            tooltip: this.getRemoveButtonTooltip(),
            cls: 'is--button-remove-rule',
            iconCls: 'sprite-minus-circle',
            border: false,
            style: {
                background: 'none',
            },
            action: 'removeRule',
            scope: this,
            handler: function () {
                this.fireEvent('removeRule', this);
            },
        });
        this.removeButtons.push(removeButton);

        return removeButton;
    },

    /**
     * @return {string} The tooltip that should be used for the receiving editor's 'remove' button.
     */
    getRemoveButtonTooltip: function () {
        return Ext.String.format(
            this.getViisonSnippet('edit/order_filter_rule_editor/header/button/remove_rule/tooltip'),
            this.getRulePath()
        );
    },

    /**
     * @return {Ext.container.Container}
     */
    createContentContainer: function () {
        var contentItems = [
            this.createRuleForm(),
        ];

        // Recursively add rule editors for the all children
        this.getChildRuleDescriptions().forEach(function (childRuleDescription, index, allChildRuleDescriptions) {
            contentItems.push(this.createChildRuleEditor(childRuleDescription));
            if (index < (allChildRuleDescriptions.length - 1)) {
                // More child rules following, hence add a conjuction type selection
                contentItems.push(this.createBooleanCompositionOperatorComboBox());
            }
        }, this);

        this.contentContainer = Ext.create('Ext.container.Container', {
            layout: this.layout,
            defaults: this.defaults,
            margin: '0',
            bodyStyle: {
                padding: '0',
                borderWidth: '0',
            },
            items: contentItems,
        });

        return this.contentContainer;
    },

    /**
     * @param {Object} ruleDescription The child rule description for the new editor.
     * @return {Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRuleEditor}
     */
    createChildRuleEditor: function (ruleDescription) {
        return Ext.create('Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRuleEditor', {
            ruleDescription: ruleDescription,
            parentEditor: this,
        });
    },

    /**
     * @return {Ext.form.field.ComboBox} A combo box providing a selection of the boolean compoisition operator.
     *         The values of all comboboxes created by the same editor are synced in realtime.
     */
    createBooleanCompositionOperatorComboBox: function () {
        return Ext.create('Ext.form.field.ComboBox', {
            cls: 'is--boolean-composition-operator-combobox',
            store: this.self.getBooleanCompositionOperatorStore(),
            queryMode: 'local',
            displayField: 'name',
            valueField: 'booleanOperator',
            value: this.ruleDescription.booleanOperator,
            maxWidth: 100,
            listeners: {
                scope: this,
                change: function (comboBox, newValue) {
                    if (!this.contentContainer) {
                        return;
                    }

                    // Update data model
                    this.ruleDescription.booleanOperator = newValue;

                    // Find and update all boolean operator comboboxes in this editor to sync the value
                    this.contentContainer.items.each(function (item) {
                        if (Ext.getClassName(item) === 'Ext.form.field.ComboBox' && item !== comboBox && item.getValue() !== newValue) {
                            item.setValue(newValue);
                        }
                    }, this);
                },
            },
        });
    },

    /**
     * @return {Ext.form.Panel}
     */
    createRuleForm: function () {
        this.ruleForm = Ext.create('Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.RuleForm', {
            ruleDescription: this.ruleDescription,
        });
        this.ruleForm.add([
            {
                xtype: 'container',
                flex: 1,
            },
            this.createRemoveButton(),
        ]);

        return this.ruleForm;
    },

    /**
     * @return {string} The readable 'path' of the receiving editor's rule. Example: '1.2' for the second child rule of
     *         the root editor's first child rule.
     */
    getRulePath: function () {
        var rulePath = this.getRuleIndex() + 1;
        if (this.getRuleNestingLevel() > 1) {
            rulePath = this.parentEditor.getRulePath() + '.' + rulePath;
        }

        return rulePath;
    },

    /**
     * @return {boolean} True, if the receiving editor's has child rules. False otherwise.
     */
    isCompositionRuleEditor: function () {
        return this.ruleDescription && 'compositionComponents' in this.ruleDescription;
    },

    /**
     * @return {Object[]} The composition components, i.e. children, of the receiving rule editor's rule, if any.
     *         An empty array otherwise.
     */
    getChildRuleDescriptions: function () {
        return (this.isCompositionRuleEditor()) ? this.ruleDescription.compositionComponents : [];
    },

    /**
     * @return {boolean} True, if the receiving rule editor's rule has any composition components, i.e. children. False
     *         otherwise.
     */
    hasChildRuleDescriptions: function () {
        return this.getChildRuleDescriptions().length > 0;
    },

    /**
     * @return {Shopware.apps.ViisonPickwareMobilePickProfiles.view.edit.OrderFilterRuleEditor[]}
     */
    getChildEditors: function () {
        return this.contentContainer.items.items.filter(function (item) {
            return Ext.getClassName(item) === Ext.getClassName(this);
        }, this);
    },

    /**
     * @return {number} The index the receiving rule editor within all sibling rule editors held by the
     *         enclosing editor. -1 if the receiving editor does not have a parent editor.
     */
    getRuleIndex: function () {
        return (this.parentEditor) ? this.parentEditor.getChildRuleDescriptions().indexOf(this.ruleDescription) : -1;
    },

    /**
     * @return {number} The nesting level of the receiving rule editor, i.e. the number of parent editors enclosing the
     *         receiving editor. 0 if the receiving editor does not have a parent editor.
     */
    getRuleNestingLevel: function () {
        return (this.parentEditor) ? (this.parentEditor.getRuleNestingLevel() + 1) : 0;
    },

    /**
     * @return {boolean} True, if this editor does not have a parent editor. False otherwise.
     */
    isRootEditor: function () {
        return this.getRuleNestingLevel() === 0;
    },

    /**
     * @return {boolean} True, if this editor can add child rules. False otherwise.
     */
    canAddChildRules: function () {
        return this.getRuleNestingLevel() < this.self.getMaxAllowedRuleNestingLevel();
    },

    /**
     * Adds a randomnly generated `uniqueId` to this component's `ruleDescription`, it it does not already contain one.
     * That ID ensures that identical rule descriptions are still treated as unique objects, e.g. in comparisons.
     */
    ensureRuleDescriptionIsUnique: function () {
        if (this.ruleDescription) {
            this.ruleDescription.uniqueId = this.ruleDescription.uniqueId || Math.floor(Math.random() * 0xFFFFFFFF).toString(16);
        }
    },

    /**
     * @return {boolean} True, if a) this editor contains a single, valid rule form or b) is the root editor or c) all
     *         child editors are valid. False otherwise.
     */
    isValid: function () {
        // Check for any child editors
        var childEditors = this.getChildEditors();
        if (childEditors.length === 0) {
            return this.ruleForm.isValid() || this.isRootEditor();
        }

        // Validate all child editors
        for (var i = 0; i < childEditors.length; i += 1) {
            if (!childEditors[i].isValid()) {
                return false;
            }
        }

        return true;
    },
});
