/*
 *  Copyright 2024 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * A mixin to have methods for widgets (such as relative fields...)
 */
Ext.define('Ametys.form.Widget', {
    
    statics: {
        /**
         * Get the value of another item
         * @param {String} relativePath The relative path to another value
         * @param {Ext.form.Field/Object} data An extjs field when in a ConfigurableFormPanel or an object with 'record' and 'dataPath'
         * @param {Object} options An object that can contain options about how to get the value (such as "silently")
         * @return {Object} The value of the field. Can be null. undefined if there is no such field. 
         */
        getRelativeValue(relativePath, data, options) {
            options = options || {};
            if (!data)
            {
                return null;
            }
            else if (data.isComponent)
            {
                // Configurable Form Panel
                let form = Ametys.form.Widget._getFormFromField(data);
                let referencedField = form.getRelativeField(relativePath, data, options.silently);
                return referencedField ? referencedField.getValue() : undefined;
            }
            else if (data.record && data.dataPath)
            {
                const SEPARATOR = "/";
                
                let resolvedPathAndRecord = this._resolveParentReferencesInRelativePath(data.dataPath, relativePath, data.record);
                let valuePath = resolvedPathAndRecord.path;
                let cursorData = resolvedPathAndRecord.record.data;
                
                // Split on /
                let paths = valuePath.split(SEPARATOR);
                let acumulator = "";
                for (let path of paths)
                {
                    if (acumulator)
                    {
                        acumulator += SEPARATOR;
                    }
                    acumulator += path;
                    
                    if (acumulator.endsWith("]"))
                    {
                        // repeater
                        let i = acumulator.indexOf("[");
                        let index = parseInt(acumulator.substring(i + 1, acumulator.length - 1));
                        cursorData = cursorData[acumulator.substring(0, i) + "_repeater"].entries[index - 1].values;
                        acumulator = "";
                    }
                }
                
                let relativeValue = cursorData[acumulator];
                if (options.silently !== true && relativeValue === undefined)
                {
                    var message = "{{i18n PLUGINS_CORE_UI_WIDGET_UNKNOWN_FIELD}}" + valuePath;
                    this.getLogger().warn(message);
                }
                return relativeValue;
            }
            else
            {
                throw new Error("getRelativeValue not supported for " + data);
            }
        },
        
        /**
         * Set the value at the dataPath
         * @param {String} valuePath The path to the value to change
         * @param {Ext.data.Model} record The record to change
         * @param {Object} value the new value
         */
        setRecordValue: function(valuePath, record, value)
        {
            const SEPARATOR = "/";
            
            let cursorData = record.data;
            
            // Split on /
            let paths = valuePath.split(SEPARATOR);
            let acumulator = "";
            
            let firstRepeaterAcumulator;
            
            for (let path of paths)
            {
                if (acumulator)
                {
                    acumulator += SEPARATOR;
                }
                acumulator += path;
                
                if (acumulator.endsWith("]"))
                {
                    // repeater
                    let i = acumulator.indexOf("[");
                    let index = parseInt(acumulator.substring(i + 1, acumulator.length - 1));
                    
                    let pointer = acumulator.substring(0, i) + "_repeater";
                    
                    if (!firstRepeaterAcumulator)
                    {
                        firstRepeaterAcumulator = pointer;
                    }
                    
                    cursorData = cursorData[pointer].entries[index - 1].values;
                    acumulator = "";
                }
            }
            
            if (firstRepeaterAcumulator)
            {
                cursorData[acumulator] = value;
                
                record.set(firstRepeaterAcumulator.substring(0, firstRepeaterAcumulator.length - "_repeater".length), record.get(firstRepeaterAcumulator));
            }
            else
            {
                record.set(acumulator, value);
            }
        },

        /**
         * Get the values corresponding the model path. Will seek in repeaters.
         * @param {Ext.data.Model} record The record to analyse
         * @param {String} modelPath The model path to consider
         * @return {Object} An object where id is the dataPath and value the corresponding value
         */
        getRecordValues: function(record, modelPath)
        {
            let values = {};
            
            const SEPARATOR = "/";
                
            // Split on /
            let paths = modelPath.split(SEPARATOR);
            seek(paths, 0, "", "", record.data, record.data);
            
            function seek(paths, index, totalAccumulator, accumulator, cursorData)
            {
                let path = paths[index];
                
                if (accumulator)
                {
                    accumulator += SEPARATOR;
                }
                if (totalAccumulator)
                {
                    totalAccumulator += SEPARATOR;
                }
                accumulator += path;
                totalAccumulator += path;
                
                if (accumulator.endsWith("]"))
                {
                    // repeater entry
                    let i = accumulator.indexOf("[");
                    let index = parseInt(accumulator.substring(i + 1, accumulator.length - 1));
                    
                    seek(paths, index + 1, totalAccumulator, "" , cursorData[accumulator.substring(0, i) + "_repeater"].entries[index - 1].values);
                }
                else if (cursorData[accumulator + "_repeater"])
                {
                    // repeater loop
                    let entries = cursorData[accumulator + "_repeater"].entries;
                    for (let entry of entries)
                    {
                        seek(paths, index + 1, totalAccumulator + "[" + entry.position + "]", "", entry.values)
                    }
                }
                else if (index < paths.length - 1)
                {
                    seek(paths, index+1, totalAccumulator, accumulator, cursorData)
                }
                else
                {
                    values[totalAccumulator] = cursorData[accumulator];
                }
            }
            
            return values;
        },
        
        /**
         * Register a listener when other values changed
         * @param {String/String[]} relativePaths The relative paths to other values
         * @param {Ext.form.Field/Object} data An extjs field when in a ConfigurableFormPanel or an object with 'grid' and 'dataPath'
         * @param {Function} handler The on change handler
         * @param {String} handler.relativePath The relative path to the value that has triggered the on change event.
         * @param {Ext.form.Field/Object} handler.data The data
         * @param {Object} handler.newValue The new value
         * @param {Object} handler.oldValue The old value
         * @param {Object} scope The scope handler. Default to the field or data.grid.
         */
        onRelativeValueChange: function(relativePaths, data, handler, scope) 
        {
            const SEPARATOR = "/";

            relativePaths = Ext.Array.from(relativePaths);
                
            if (!data)
            {
                throw new Error("Cannot register a relative value change on a null component");
            }
            else if (data.isComponent)
            {
                let form = Ametys.form.Widget._getFormFromField(data);
                for (let relativePath of relativePaths)
                {
                    // Configurable Form Panel
                    function proxyHandler(field, newValue, oldValue)
                    {
                        // do not simple call handler() to keep the 'this' scope
                        handler.call(this, relativePath, data, newValue, oldValue);
                    }
                    form.onRelativeFieldsChange(relativePath, data, proxyHandler, scope);
                }
            }
            else if (data.grid && data.dataPath)
            {
                for (let relativePath of relativePaths)
                {
                    let parentPath = data.dataPath.substring(0, data.dataPath.lastIndexOf(SEPARATOR));
                    let valuePath = this._resolveParentReferences(parentPath, relativePath);
                    
                    data.grid.changeListeners = data.grid.changeListeners || {};
                    data.grid.changeListeners[valuePath] = data.grid.changeListeners[valuePath] || [];
                    data.grid.changeListeners[valuePath].push(function(localData, newValue, oldValue) {
                        handler.call(this, relativePath, {grid: data.grid, dataPath: data.dataPath, record: localData.record}, newValue, oldValue);
                    });
                }
            }
            else
            {
                throw new Error("onRelativeValueChange not supported for " + data);
            }
        },
        
        /**
         * Evaluate the disable conditions of the field now
         * @param {Ext.form.Field/Object} data An extjs field or the object with {disableCondition, dataPath and record}
         * @return {Boolean} Should disable? 
         */
        evaluateDisableCondition: function(data) 
        {
            if (data.disableCondition === 'string')
            {
                data.disableCondition = JSON.parse(data.disableCondition, data);
            }
            
            return Ametys.form.Widget._evaluateDisableCondition(data.disableCondition, data).disabled;
        },
        
        /**
         * @private
         * Evaluates the disable condition when a matching field is changing and enables/disables the field accordingly.
         * @param {Object} disableCondition the disable condition.
         * @param {Ext.form.Field/Object} data An extjs field or the object with grid info
         * @return {Object} the result of the given disable condition
         * @return {Boolean} return.disabled true if the disable condition is verified, false otherwise.
         * @return {Boolean} return.hidden true if the field must be hidden due to one verified condition
         */
        _evaluateDisableCondition: function(disableCondition, data)
        {
            const EXTERNAL_CONDITION_PREFIX = "__external";
            
            if (!disableCondition || !disableCondition.conditions && !disableCondition.condition)
            {
                return {
                    disabled: false,
                    hidden: false
                };
            }
            
            let disable = disableCondition['type'] != "and" ? false : true;
            let hidden = false;
            
            if (disableCondition.conditions)
            {
                var conditionsList = disableCondition.conditions,
                    conditionsListLength = conditionsList.length;
                
                for (var i = 0; i < conditionsListLength; i++)
                {
                    var result = Ametys.form.Widget._evaluateDisableCondition(conditionsList[i], data);
                    disable = disableCondition['type'] != "and" ? disable || result.disabled : disable && result.disabled;
                    hidden = hidden || result.hidden;
                }
            }
            
            if (disableCondition.condition)
            {
                var conditionList = disableCondition.condition,
                    conditionListLength = conditionList.length;
                
                for (var i = 0; i < conditionListLength; i++)
                {
                    var id = conditionList[i]['id'],
                        op = conditionList[i]['operator'],
                        val = conditionList[i]['value'],
                        hideIfDisabled = conditionList[i]['hideIfDisabled'];
                        
                    var result = Ametys.form.Widget._evaluateCondition(id, op, val, data);
                    disable = disableCondition['type'] != "and" ? disable || result : disable && result;
                    
                    if (result && (id.startsWith(EXTERNAL_CONDITION_PREFIX) || hideIfDisabled))
                    {
                        hidden = true;
                    }
                }
            }
            
            return {
                disabled: disable,
                hidden: hidden
            };
        },
        
        /**
         * @private
         * Evaluates a single condition.
         * @param {String} id the id of the field.
         * @param {String} operator the operator.
         * @param {String} untypedValue the untyped value the field's value will be compared to.
         * @param {Ext.form.Field/Object} data An extjs field or the object with grid info
         * @return {Boolean} result true if the condition is verified, false otherwise.
         */
        _evaluateCondition: function(id, operator, untypedValue, data)
        {
            let type;
            const EXTERNAL_CONDITION_PREFIX = "__external";
            
            if (data.isComponent)
            {
                let form = Ametys.form.Widget._getFormFromField(data);
                if (id.startsWith(EXTERNAL_CONDITION_PREFIX))
                {
                    let fieldName = (data.isRepeater ? data.prefix : '') + data.name;
                    let dataName = this._getDataName(form, fieldName);
                    let completeId = id + "_" + dataName;
                    
                    let externalDisableConditionsValues = this._getExternalDisableConditionsValuesFromForm(form, fieldName);
                    return externalDisableConditionsValues.hasOwnProperty(completeId)
                        ? externalDisableConditionsValues[completeId]
                        : false;
                }
                
                let formField = form.getRelativeField(id, data);
                if (!formField)
                {
                    return false; // no field, not disabled...
                }
                type = formField.type;
            }
            else if (data.record)
            {
                let record = data.record;
                if (id.startsWith(EXTERNAL_CONDITION_PREFIX))
                {
                    let externalDisableConditionsValues = Ametys.form.Widget._getExternalDisableConditionsValuesFromRecord(record);
                    let completeId = id + "_" + data.dataPath;
                    
                    return externalDisableConditionsValues && externalDisableConditionsValues.hasOwnProperty(completeId)
                        ? externalDisableConditionsValues[completeId]
                        : false;
                }
                
                let resolvedPathAndRecord = this._resolveParentReferencesInRelativePath(data.dataPath, id, record);
                let localId = resolvedPathAndRecord.path;
                record = resolvedPathAndRecord.record;
                
                let field = record.getField(localId);
                if (!field)
                {
                    return false; // no field, not disabled...
                } 
                type = field.ftype || field.type;
            }
            else
            {
                throw new Error("Disable conditions not supported for " + data);
            }
            
            var fieldValue = Ametys.form.Widget.getRelativeValue(id, data);
            fieldValue = Ametys.form.widget.Externalizable.getValueInUse(fieldValue);
            
            var typedValue = Ametys.form.Widget._convertUntypedValue(type, untypedValue);

            if (fieldValue === undefined || fieldValue === "")
            {
                fieldValue = null;
            }
            if (typedValue === undefined || typedValue === "")
            {
                typedValue = null;
            }
                         
            switch (operator)
            {
                case "gt" : 
                    if (!Ext.isArray(fieldValue))
                    {
                        return fieldValue > typedValue;
                    }
                    else
                    {
                        // All entries need to match operator
                        for (var i = 0; i < fieldValue.length; i++)
                        {
                            if (!(fieldValue[i] > typedValue))
                            {
                                // One entry does not match!
                                return false;
                            }
                        }
                        return true;
                    }
                case "geq" : 
                    if (!Ext.isArray(fieldValue))
                    {
                        return fieldValue >= typedValue;
                    }
                    else
                    {
                        // All entries need to match operator
                        for (var i = 0; i < fieldValue.length; i++)
                        {
                            if (!(fieldValue[i] >= typedValue))
                            {
                                // One entry does not match!
                                return false;
                            }
                        }
                        return true;
                    }
                case "lt" : 
                    if (!Ext.isArray(fieldValue))
                    {
                        return fieldValue < typedValue;
                    }
                    else
                    {
                        // All entries need to match operator
                        for (var i = 0; i < fieldValue.length; i++)
                        {
                            if (!(fieldValue[i] < typedValue))
                            {
                                // One entry does not match!
                                return false;
                            }
                        }
                        return true;
                    }
                case "leq" : 
                    if (!Ext.isArray(fieldValue))
                    {
                        return fieldValue <= typedValue;
                    }
                    else
                    {
                        // All entries need to match operator
                        for (var i = 0; i < fieldValue.length; i++)
                        {
                            if (!(fieldValue[i] <= typedValue))
                            {
                                // One entry does not match!
                                return false;
                            }
                        }
                        return true;
                    }
                case "neq" : 
                    if (!Ext.isArray(fieldValue))
                    {
                        return fieldValue  !== typedValue; // null and empty string should be considered the same
                    }
                    else
                    {
                        // All entries need to match operator
                        for (var i = 0; i < fieldValue.length; i++)
                        {
                            if (!(fieldValue[i] !== typedValue))
                            {
                                // One entry does not match!
                                return false;
                            }
                        }
                        return true;
                    }
                case "eq" : 
                    if (!Ext.isArray(fieldValue))
                    {
                        return fieldValue === typedValue;
                    }
                    else
                    {
                        // One entry needs to match operator
                        for (var i = 0; i < fieldValue.length; i++)
                        {
                            if (fieldValue[i] === typedValue)
                            {
                                // One entry does match!
                                return true;
                            }
                        }
                        return false;
                    }
                default :
                    throw "Unknown operator " + operator;
            }
        },
        
        shouldHideDisabledField: function(data)
        {
            // First check on the data if the disabled behavior is forced
            if (data.disabledItemRendering == "hidden")
            {
                return true;
            }
            else if (data.disabledItemRendering == "disabled")
            {
                return false;
            }
            
            // Then, check if the hide option is set to true on form
            else if (data.isComponent && Ametys.form.Widget._getFormFromField(data).hideDisabledFields)
            {
                return true;
            }
            
            // In the end, check if there is a verified condition configured to hide the field
            if (data.disableCondition === 'string')
            {
                data.disableCondition = JSON.parse(data.disableCondition, data);
            }
            return Ametys.form.Widget._evaluateDisableCondition(data.disableCondition, data).hidden;
        },
        
        /**
         * @private
         * Retrieves the name of the data that will be used as suffix in condition identifier
         * @param {{Ext.form.Panel} form the form
         * @param {String} fieldName the name of the field with the condition
         * @return {String} the name of the data that will be used as suffix in condition identifier
         */
        _getDataName: function(form, fieldName)
        {
            if (fieldName.includes("["))
            {
                let pathSegments = fieldName.split(form.defaultPathSeparator);
                let indexOfRepeater = this._getLastRepeaterIndex(pathSegments);
                
                let dataName = '';
                for (let i = indexOfRepeater + 1; i < pathSegments.length; i++)
                {
                    if (i > indexOfRepeater + 1)
                    {
                        dataName += form.defaultPathSeparator;
                    }
                    dataName += pathSegments[i];
                    
                }
                
                return dataName;
            }
            else
            {
                return fieldName.startsWith(form.fieldNamePrefix)
                            ? fieldName.substring(form.fieldNamePrefix.length)
                            : fieldName;
            }
        },
        
        /**
         * @private
         * Retrieves the external disable condition values from the given form
         * @param {{Ext.form.Panel} form the form
         * @param {String} fieldName the name of the field with the condition
         * @return {Object} the external disable condition values from the given form
         */
        _getExternalDisableConditionsValuesFromForm: function(form, fieldName)
        {
            if (fieldName.includes("["))
            {
                let startsWithPrefix = fieldName.startsWith(form.fieldNamePrefix);
                let fieldNameWithoutPrefix = startsWithPrefix
                                           ? fieldName.substring(form.fieldNamePrefix.length)
                                           : fieldName;
                
                let pathSegments = fieldNameWithoutPrefix.split(form.defaultPathSeparator);
                let indexOfRepeater = this._getLastRepeaterIndex(pathSegments);
                
                let repeaterSegment = pathSegments[indexOfRepeater];
                let repeaterName = repeaterSegment.substring(0, repeaterSegment.lastIndexOf("["));
                let repeaterItemPosition = parseInt(repeaterSegment.substring(repeaterSegment.lastIndexOf("[") + 1, repeaterSegment.lastIndexOf("]")));

                let repeaterPrefix = startsWithPrefix ? form.fieldNamePrefix : '';
                for (let i = 0; i < indexOfRepeater; i++)
                {
                    repeaterPrefix += pathSegments[i] + form.defaultPathSeparator;
                }
                
                let repeater = form.getRepeaters().filter(function(r) {
                    return r.name == repeaterName && r.prefix == repeaterPrefix;
                })[0];
                
                return repeater.getItemExternalDisableConditionsValues(repeaterItemPosition - 1);
            }
            else
            {
                return form.getExternalDisableConditionsValues();
            }
        },
        
        /**
         * @private
         * Retrieves the index of the last segment representing a repeater entry
         * @param {String[]} pathSegments the path segments
         * @return {Object} the index of the last segment representing a repeater entry
         */
        _getLastRepeaterIndex(pathSegments)
        {
            let indexOfRepeater = pathSegments.length - 1;
            let pathSegment = pathSegments[indexOfRepeater];
            while (indexOfRepeater > -1 && !pathSegment.includes("["))
            {
                indexOfRepeater--;
                pathSegment = pathSegments[indexOfRepeater]
            }
            
            return indexOfRepeater;
        },
        
        /**
         * @private
         * Retrieves the external disable conditions values from the given record
         * @param {Ext.data.Model} record the record
         * @return the external disable conditions values from the given record
         */
        _getExternalDisableConditionsValuesFromRecord: function(record)
        {
            let externalDisableConditionsValues = record.get("__externalDisableConditionsValues");
            if (externalDisableConditionsValues === undefined)
            {
                // If not found in the record, it may be in the temporary data used by ContentViewTreeGridPanel
                // This can happen at content grid loading
                let data  = record.get("data");
                if (data && data.hasOwnProperty("__externalDisableConditionsValues"))
                {
                    externalDisableConditionsValues = data["__externalDisableConditionsValues"];
                }
            }
            
            if (externalDisableConditionsValues === undefined)
            {
                // If not found, it may be in properties
                let properties  = record.get("properties");
                if (properties && properties.hasOwnProperty("__externalDisableConditionsValues"))
                {
                    externalDisableConditionsValues = properties["__externalDisableConditionsValues"];
                }
            }

            if (externalDisableConditionsValues === undefined)
            {
                // If not found, it may be in parent record
                let parentRecord = Ametys.plugins.cms.search.SearchGridHelper._getParentRecord(record);
                if (parentRecord)
                {
                    externalDisableConditionsValues = Ametys.form.Widget._getExternalDisableConditionsValuesFromRecord(parentRecord);
                }
            }
            
            return externalDisableConditionsValues;
        },
        
        /**
         * @private
         * Resolve the parent references (..) in the relative path. Retrieves the resolved path and the relative record
         * ex1: content of type exhaustive, record represents the content, the data path is composite/repeater/comp-string, the relative path is ../../boolean
         * the returned path is boolean, and record represents the content
         * ex2: content of type exhaustive, record represents the composite/repeater, the data path is comp-string, the relative path is ../../boolean
         * the returned path is boolean, and record represents the content
         * ex2: content of type exhaustive, record represents the repeaterThreeLevels/repeater2/repeater3, the data path is rep3-string, the relative path is ../../rep-boolean
         * the returned path is rep-boolean, and record represents the repeater of first level
         * @param {String} dataPath the path of the data relative to the given record 
         * @param {String} relativePath the path relative to base data path
         * @param {Ext.data.Model} record the record containing the data at the given base path
         * @return {Object} an object containing the absolute path (absolutePath) and the record containing the data at the absolute path (record)
         */
        _resolveParentReferencesInRelativePath: function (dataPath, relativePath, record)
        {
            const SEPARATOR = "/";
            
            let localPath = relativePath;
            let localRecord = record;
            if (localPath.startsWith('..' + SEPARATOR))
            {
                let parentPath = Ametys.plugins.cms.search.SearchGridHelper._getParentMetadataPath(localRecord);
                if (parentPath)
                {
                    localRecord = Ametys.plugins.cms.search.SearchGridHelper._getParentRecord(localRecord);
                    
                    // Compute the parent path, relative to the parent record
                    // ex: parentPath is repeater1/composite1/repeater2, parent record is repeater1, relativeParentPath is composite1/repeater2
                    let parentParentPath = Ametys.plugins.cms.search.SearchGridHelper._getParentMetadataPath(localRecord);
                    let relativeParentPath = parentParentPath ? parentPath.substring(parentParentPath.length + SEPARATOR.length) : parentPath;
                    
                    localPath = this._resolveParentReferences(relativeParentPath, localPath);
                                                
                    return this._resolveParentReferencesInRelativePath(dataPath, localPath, localRecord);
                }
                else
                {
                    // No parent data path => localRecord is root
                    if (dataPath.includes(SEPARATOR))
                    {
                        let parentPath = dataPath.substring(0, dataPath.lastIndexOf(SEPARATOR))
                        localPath = this._resolveParentReferences(parentPath, localPath);
                    }
                }
            }
            
            return {
                path: localPath,
                record: localRecord
            };
        },
       
        /**
         * @private
         * Resolve the parent references(..) in the given relativePath
         * The parent references are removed and necessary parent path segment are added to the retrieved path
         * @param {String} parentPath the parent path of the data relative to the relative path
         * @param {String} relativePath the path relative to base data path
         * @return {String} the relative path with resolved parent references 
         */ 
        _resolveParentReferences: function(parentPath, relativePath)
        {
            const SEPARATOR = "/";
            
            let localPath = relativePath;
            let parentPathSegments = parentPath.split(SEPARATOR);
            for (let i = parentPathSegments.length - 1 ; i >= 0 ; i--)
            {
                if (localPath.startsWith('..' + SEPARATOR))
                {
                    localPath = localPath.substring(('..' + SEPARATOR).length);
                }
                else
                {
                    localPath = parentPathSegments[i] + SEPARATOR + localPath;
                }
            }
            
            return localPath;
        },
        
        /**
         * @private
         * Converts an untyped value to a typed value according the given type
         * @param {String} fieldType The type to convert into
         * @param {String} untypedValue The untyped value, as a String
         * @return The typed value
         */
        _convertUntypedValue: function (fieldType, untypedValue)
        {
            switch (fieldType) {
                case "boolean":
                    if (Ext.isString(untypedValue))
                    {
                        return untypedValue.toLowerCase() === "true"
                    }
                    else if (untypedValue === undefined || untypedValue === null)
                    {
                        return untypedValue;
                    }
                    return !!untypedValue;
                case "long":
                    return parseInt(untypedValue);
                case "double":
                    return parseFloat(untypedValue);
                case "date":
                    return Ext.Date.parse(untypedValue, Ext.Date.patterns.ISO8601DateTime);
                default:
                    return untypedValue;
            }
        },
        
        /**
         * @private
         * Retrieves the form from the given field
         * @param {Ext.form.Field} field The extjs field
         * @return the form containing the given field
         */
        _getFormFromField: function(field)
        {
            let form = field.form;
            
            if (!form)
            {
                // repeater for example
                form = field;
                while (!form.isConfigurableFormPanel)
                {
                    form = form.ownerCt;
                }
            }
            
            return form;
        },
            
        /**
         * @public
         * Creates a disble condition with given options and add it to the given field
         * @param {Object} field the field that will have the new condition
         * @param {String} conditionId The id of condition (the path to the referenced field in case of relative condition)
         * @param {String} operator the condition's operator
         * @param {String} value The value to check for the condition
         * @param {Boolean} hideIfDisabled true if the field must be hidden when the new condition is verified. Default to false
         * @param {String} type type of disabled conditions that will wrap the existent conditions and the new one. Can be 'or' or 'and'. Default to 'or'
         * @return {Object} The object for the disable condition
         */
        addDisableCondition: function(field, conditionId, operator, value, hideIfDisabled, type)
        {
            let newCondition;

            let existingCondition = field['disableCondition'];
            if (!Ext.Object.isEmpty(existingCondition) && existingCondition.condition)
            {
                newCondition = {
                    type: type || 'or',
                    condition: [{
                        id: conditionId,
                        operator: operator,
                        value: value,
                        hideIfDisabled: hideIfDisabled || false
                    }],
                    conditions: [existingCondition]
                }
            }
            else
            {
                newCondition = {
                    condition: [{
                        id: conditionId,
                        operator: operator,
                        value: value,
                        hideIfDisabled: hideIfDisabled || false
                    }]
                };
            }
            
            field['disableCondition'] = newCondition;
        },
    }
});