/*
* @property {boolean} editable - флаг, показывает можно ли изменять модель (добавлять/обновлять).

    В чем проблема с binded полями?
Есть поле deviceID, которое получаем из SELECT. Поле deviceID нужно отправить на сервер.
Но при построении интерфейса нужно больше:
- объект device, 
    чтобы вывести имя объекта в списке ListModel, например "Sharky 775 (m-bus)"
    чтобы заполнить addr SELECT, который зависит от device.connector.connectorID
- список объектов из device SELECT, чтобы заполнить SELECT без обращения к серверу, 
когда юзер будет кликать по модемам, пытаясь что-то отредактировать.
- deviceID, чтобы отправить на сервер как поле объекта и чтобы выбрать в SELECT 
нужный пункт, когда юзер щелкает пытаясь что-то отредактировать.

Список можно не хранить, а загружать снова с сервера. 
Но объект и ID хранить обязательно.

Либо можно вызывать обработчик, которому передавать объект, а тот в свою очередь
будет строить addr SELECT, либо внедрит поле 'name' в formobj, которое будет 
выводится как заголовок в списке ListModel. Минус - на сервер будет отправляться
поле 'name', которое там не ждут. 

Можно пойти еще дальше. Обработчик внедряет поле '_name', с префиксом '_'. Когда мы 
получаем полный объект для отправки на сервер (Форма без привязанной модели) - рекурсивно
обходит полученный объект и удаляем все служебные поля с префиксом '_'

*/

function Formobj(opt) { 
    Emiter.call(this)

    // Родительская форма
    this._parent = opt.parent
    this._isEditable = opt.editable
    this._form = getElemSafe('form', opt.form)

    //console.log('BUILD:', this._form)
    //
    this._errorClass = opt.errorClass || 'errmsg'
    this._validate = opt.validate

        // Для FIX
    this._version = 0 

    //this._embed = opt.embed || {}
    this._fields = {}

    this._predefined = opt.predefined || {}
    //this._schema = opt.schema || {}
    //this._nonUpdatable = opt.nonUpdatable || {}
    this._transformData = opt.transformData
    this._userOnSubmit = opt.onSubmit
    // result - успешный ответ API функции submitFunc
    // API ошибка автоматически вызовет showError
    this._onSubmitResult = opt.onSubmitResult


    if (opt.model) {
        this._model = opt.model
        this._model.on('select', obj => this.setObj(obj))
    } else if (opt.models) {
        this._models = opt.models
        this._getModel = opt.getModel
        
        if (typeof opt.getModel != 'function') {
            throw new Error(`'models' option requires 'getModel' option`)
        }

        for (let prop in this._models) {
            let model = this._models[prop]
            model.on('select', obj => {
                if (obj) {
                    // снимаем выделение с других моделей, если есть
                    for (let modelKey in this._models) {
                        let item = this._models[modelKey]
                        if (modelKey != prop) {
                            console.log('UNSELECT:', modelKey)
                            item.unselect()
                        }
                    }
                }
                this.setObj(obj)
            })
        }
    }

    this._tree = this._buildTree(opt)
    console.log('TREE:', this._tree)




    //this._plain = x.plain
    //this._custom = x.custom
    //this._embed = x.embed

    this._form.addEventListener('submit', (e) => {
        e.preventDefault()
        this._onSubmit()
    })
    
    if (opt.submit) {
        this._submit = getElemSafe('submit', opt.submit)
    } else {
        // Поиск submit-кнопки
        for (let i=0; i<this._form.elements.length; i++) {
            let elem = this._form.elements.item(i)
            if (elem.getAttribute('type') == 'submit') {
                this._submit = elem
            }
        }
    }
    //if (!this._submit) {
    //    throw new Error('submit button not found')
    //}

    if (this._submit) {
        if (this._submit.tagName == 'INPUT') {
            this._btnSubmitText = this._submit.value
        } else if (this._submit.tagName == 'BUTTON') {
            this._btnSubmitText = this._submit.textContent
        }
    
        this._isSubmitDisabledByDefault = this._submit.disabled
    }

    
    this._btnUpdateText = opt.btnUpdateText || this._btnSubmitText

    this._api = opt.api
    //this._updateFunc = opt.updateFunc
    if (opt.submitFunc) {
        this._getSubmitFunc = function() {
            return opt.submitFunc
        }
    } else if (opt.getSubmitFunc) {
        this._getSubmitFunc = opt.getSubmitFunc
    }
    //this._submitFunc = opt.submitFunc
    //this._removeFunc = opt.removeFunc

    if (opt.removeFunc) {
        this._getRemoveFunc = function() {
            return opt.removeFunc
        }
    } else if (opt.getRemoveFunc) {
        this._getRemoveFunc = opt.getRemoveFunc
    }

    this._getRemoveData = opt.getRemoveData
    this._btnRemoveText = opt.btnRemoveText
    this._btnRemoveClass = opt.btnRemoveClass
    this._key = opt.key

    /*
    if (this._getRemoveFunc) {
        if (!this._model) {
            throw new Error(`'removeFunc' option requires 'model' option`)
        }
        //if (!this._) {
            //throw new Error(`'removeFunc' option requires 'key' option`)
        //}
        
    }
    */

    
    if (opt.btnRemove) {
        this._btnRemove = getElemSafe('btnRemove', opt.btnRemove)
        if (!this._getRemoveFunc) {
            throw new Error(`'btnRemove' option requires 'removeFunc' option`)
        }
        this._isBtnRemoveDisabledByDefault = this._btnRemove.disabled

        this._btnRemove.addEventListener('click', () => {
            
            // На модель не проверяем, ибо уже проверили в конструкторе
            let selected = this._model.getSelected()
            if (selected) {
                let p = {}

                if (this._getRemoveData) {
                    p = this._getRemoveData(selected)
                } else {
                    let id = selected[this._key]
                    
                    //p[this._key] = id
                    p = id // data - ключ вместо объекта
                }

                this._btnRemove.disabled = true

                let url = this._getRemoveFunc(selected)

                this._api.req({
                    func: url,
                    data: p,
                    onError: err => {
                        this._btnRemove.disabled = false
                        this.showError(err)
                    },
                    onSuccess: resp => {
                        if (this._model) {
                            // Именно перезагружаем, чтобы избежать ошибок, если модель с параметром.
                            console.log('model reload, form reset (removeFunc)')
                            this._model.reload()
                            this.reset()
                        } else {
                            if (this._isSubmitDisabledByDefault) {
                                this._calcSignature()
                            }
                            if (this._submit) {
                                this._submit.disabled = this._isSubmitDisabledByDefault
                            }
                            this._btnRemove.disabled = this._isBtnRemoveDisabledByDefault
                        }
                        // FIX
                        if (this._userOnSubmit) {
                            this._userOnSubmit()
                        }
                    }
                })
                return
            }
        })
    }
/*
    if (this._updateFunc) {
        if (!this._api) {
            throw new Error("'updateFunc' requires 'api' option")
        }
    }
*/
    if (this._getSubmitFunc) {
        if (!this._api) {
            throw new Error("'submitFunc' requires 'api' option")
        }
    }

    // добавить setifdisabled
    this._createObjects(this._tree)


    if (this._isSubmitDisabledByDefault) {
        //this._listenChanges()
        this._calcSignature()
    }


    // init custom elements

    //console.log('INIT:', this._tree.custom)
    
    for (let prop in this._tree.custom) {
        let c = this._tree.custom[prop]
        if (c.getFunc) {
            this._loadData(c)
        }
    }
}

Formobj.prototype = Object.create(Emiter.prototype)
Formobj.prototype.constructor = Formobj

Formobj.EDITABLE = 1
Formobj.READONLY = 2
Formobj.DISABLED = 3

Formobj.prototype.isDisabled = function() {
    /*
    if (this._parent) {
        return this._parent.isDisabled()
    }
    if (this._model && !this._isEditable && this._model.getSelected()) {
        return true
    }
    return false
    */
    if (this._isEditable) {
        return false
    } else {
        if (this._settingObj) {
            return true
        }
        return false
    }
}

/*
Formobj.prototype.restoreState = function() {
    let stateStr = localStorage.getItem('x')
    if (stateStr) {
        let state = JSON.parse(stateStr)

        console.log('SET STATE:', stateStr)
        this.setObj(state)
    }
}
*/

// po - parent object
Formobj.prototype._loadData = function(item, po, id) {
    //console.log('_loadData:', item, po, id)
    // В кэше нет - загружаем        
    let version = this._version
    if (this._settingObj) {
        this._settingObj.loadingCount++
    }

    item.loading = true

    this._api.req({
        func: item.getFunc(po),
        data: id,
        onError: err => {       
            item.loading = false     
            if (version !== this._version) {
                //console.log('version changed! this._version != version', this._version, version)
                return
            }
            item.divLoadError = insertError({
                after: item.elem.getAnchor(), 
                err: err,
                class: this._errorClass
            })
            //console.log("load:", err)
            if (this._settingObj) {
                this._settingObj.loadingCount--
                this._onFormLoaded()
            }
        },
        onSuccess: resp => {
            item.loading = false
            if (version !== this._version) {
                //console.log('version changed! this._version != version', this._version, version)
                return
            }
            // Добавляем в кэш
            // fix перенести в Attached
            //let m = this._cache[item.prop]
            //m.set(id, resp)

            let cleanResp = resp
            if (item.responseModifier) {
                cleanResp = item.responseModifier(resp)
            }

            item.cache.set(id, cleanResp)

            item.elem.setList(cleanResp)

            this._setValueIfEditMode(item)

            if (this._settingObj) {
                this._settingObj.loadingCount--
                this._onFormLoaded()
            }
        }
    })
}

Formobj.prototype._onFormLoaded = function() {
    if (this._settingObj.loadingCount == 0) {
        //console.log('_onFormLoaded:', this, this._isSubmitDisabledByDefault)
        if (this._isSubmitDisabledByDefault) {
            this._calcSignature()
        }
        this._settingObj = null
    }    
}

Formobj.prototype._calcSignature = function() {
    // Нет проблемы если AJAX элементы не загрузились.
    // Все 'list' элементы вернут [], все 'select' элементы вернут undefined
    this._signature = JSON.stringify(this.cleanObj(this.getFormData()))
    //this._signature = JSON.stringify(this.getFormData())
    console.log('sign:', this._signature)
}
/*
Formobj.prototype._listenChanges = function() { 
    //console.log('listenChanges: ', this, JSON.stringify(this._tree)) 
    if (!this._isSubmitDisabledByDefault) {
        return
        
    }  

    let x = this._tree



    // Повесим на все элементы формы обработчики FIX
    for (let i=0; i<this._form.elements.length; i++) {
        let elem = this._form.elements[i]

        //console.log('elem:', elem.name)
        //console.log('x.plain:', x.plain)
        //console.log('x.keys:', Object.keys(x))
        // Если такого поля нет в схеме
        if (x.plain && !x.plain[elem.name]) {
            continue
        }
        let eventName = 'input' // для input, textarea
        // Чтобы избежать багов в разных браузерах - для select, checkbox и
        // radio - повесим обработчик на другое событие - 'change'
        if (elem.nodeName == 'SELECT') {
            eventName = 'change'
        } else if (elem.nodeName == 'INPUT') {
            if (elem.type == 'radio' || elem.type == 'checkbox') {
                eventName = 'change'
            }
        }
        elem.addEventListener(eventName, () => this._onAnyChange())
    }

    for (let prop in x.embed) {
        x.embed[prop].model.on('listChanged', () => {
            //console.log('listChanged -> on any change')
            this._onAnyChange()
        })
    }

    // только топ уровень
    if (x.custom) {
        //let items = this._attachedObj.listItems()
        for (let prop in x.custom) {
            let a = x.custom[prop]
            //console.log('listen select on:', a.type)
            switch (a.elem.getType()) {
            case 'select':
                // Select, RadioList
                a.elem.on('select', () => this._onAnyChange())
                break;

            case 'list':
                // InputList, CheckList
                a.elem.on('changed', () => this._onAnyChange())
                break;
            }                
        }
    }

    this._calcSignature()
}
*/


Formobj.prototype._onAnyChange = function() {
    //console.log('select changed:', this.isDisabled(), JSON.stringify(this.cleanObj(this.getFormData())))
    
    //if (this._loadingCount > 0) {
     //   console.log('loadingCount > 0')
    //    return
    //}
    if (this._reseting) {
        //console.log('reseting:', this._reseting, this._form)
        return
    }
    if (this._settingObj) {
        //console.log('settingObj:', this._settingObj, this._form)
        return
    }
    if (this.isDisabled()) {
        //console.log('disabled:', this._form)
        return
    }
    //console.log('anyChange current:', this._signature)
    //console.log('anyChange     new:', JSON.stringify(this.cleanObj(this.getFormData())))
    let stateStr = JSON.stringify(this.cleanObj(this.getFormData()))

    //console.log('anyChange current sign:', this._signature)
    //console.log('anyChange current:', stateStr)

    if (this._signature != stateStr) {
        this._submit.disabled = false
    } else {
        this._submit.disabled = true
    }

    //localStorage.setItem('x', stateStr)

    // fix
    //console.log("form changed")
    //this.emit('change')
}

//Formobj.prototype.isDisabled = function() {
//}

Formobj.prototype._setValueIfEditMode = function(item) {
    //console.log('!!!!!!!!!! setValueIfEditMode:', item)
    
    // если режим редактирования
    if (this._settingObj) {
        
        if (item.manage) {
            //console.log('setValueIfEditMode', item)
            let nestedObj = this._settingObj.obj[item.prop]
            // удалили, после выбора юзером другого пункта в процессе редактирования
            //console.log('!!!!!!!_setValueIfEditMode:', this._version, item, nestedObj)
            //if (nestedObj) {
                switch (item.elem.getType()) {
                case 'select':
                    //let nestedObj = this._settingObj.obj[item.prop]
                    if (nestedObj) {
                        //console.log('select (edit mode): ', nestedObj)
                        if (item.key) {
                            //console.log('SELECT: ', nestedObj[b.key], b.elem)
                            item.elem.select(nestedObj[item.key])
                        } else {
                            item.elem.select(nestedObj)
                        }
                    }
                    //item.elem.select(nestedObj[item.key])
                    break
                    
                case 'list':
                    // Хитрый код для такого кейса:
                    // отмечаем пункты галочками - на сервер уходит массив ключей, например,
                    // commandIDs. С сервера получаем массив объектов commands. Чтобы отметить
                    // галочками нужные пункты при setObj, нужно знать имя поля (commands) с
                    // массивом объектов, а также имя поля с ID объекта (commandID). Для этой
                    // служат опции setProp и setKey

                    if (nestedObj) {
                        // Просмотр добавленого через JS, до отправки на сервер
                        item.elem.setValues(nestedObj)
                    } else if (item.setProp && item.setKey) {
                        let objects = this._settingObj.obj[item.setProp]
                        if (objects) {
                            let keys = []
                            objects.forEach(o => {
                                keys.push(o[item.setKey])
                            })
                            item.elem.setValues(keys)
                        }
                    }                    
                    break;

                case 'input':                    
                    if (nestedObj) {
                        if (item.key) {
                            item.elem.setValue(nestedObj[item.key])
                        } else {
                            item.elem.setValue(nestedObj)
                        }
                        //item.elem.setValue(nestedObj)
                    }
                    break;
                }

               // let obj = this._settingObj.obj


                let t = item.tree
                if (t) {
                    //console.log('tree:', t)
                    if (t.current) {
                        this._setObj(t.current)
                    }
                }

                // отключаем зависимые элементы, которые depends от кого-то
                /*
                if (this.isDisabled()) {
                    item.elem.disable()
                }
                */
                
            //}
        }

        // отключаем независимые attached элементы (у которых нет depends),
        if (this._isEditable) {
            // Если форма редактируемая, а поле нет - отключаем
            if (item.editmode != Formobj.EDITABLE) {
                item.elem.disable()
            }
        } else {
            // Если форма НЕ редактируемая - просто отключаем
            item.elem.disable()
        }
        
/*
        if (this.isDisabled()) {
            item.elem.disable()
        } else {
            if (item.readonly) {
                item.elem.disable()
            }
            // WARNING! NEW FUTURE
            // SCHEMA FUTURE
            
            //if (this._nonUpdatable[item.prop]) {
            //    item.elem.disable()
           // }
            
        }
        */
    }
}

/*
Formobj.prototype.setIsDisabledFunc = function(f) {
    this.isDisabled = f

    for (let prop in this._tree.embed) {
        let embed = this._tree.embed[prop]
        embed.form.setIsDisabledFunc(f)

        if (embed.model) {
            embed.model.setIsDisabledFunc(f)
        } else {
            for (let modelKey in embed.multiModel) {
                embed.multiModel[modelKey].setIsDisabledFunc(f)
            }
        }
    }
}
*/

Formobj.prototype._resetTree = function() {
}

Formobj.prototype._resetFields = function(x) {
    if (x.schema) {
        for (let prop in x.schema) {
            let f = x.schema[prop]
            //if (elem)
            //console.log('RESET prop:', prop, 'value:', f.defaultValue)
            f.elem.value = f.defaultValue
            f.elem.disabled = false
        }
    }
    

    if (this._disabledElems) {
        this._disabledElems.forEach((elem) => {
            //console.log('ENABLE:', elem.name)
            elem.disabled = false
        })
        this._disabledElems = null
    }

    if (this._readonlyElems) {
        this._readonlyElems.forEach((elem) => {
            elem.readOnly = false
        })
        this._readonlyElems = null
    }

    // reset custom
    if (x.custom) {
        for (let prop in x.custom) {
            let c = x.custom[prop]
            // независимые элемены сбрасываем, а зависимые очищаем
            if (c.manage) {
                c.elem.reset()
            }
            // делаем unlock для независимых элементов
            // зависимые очищаются (clear), поэтому enable делать не смысла
            c.elem.enable()

            //c.chains.forEach((nested) => {
            //    this._clearDepended(nested)
            //})  
            let t = c.tree
            if (t) {
                if (t.current) {
                    this._resetFields(t.current)
                }
                //for (let prop in c.tree) {
                    //let t = c.tree[prop]
                    //console.log('reset tree:', t)
                //}
            }
        }
    }

    for (let prop in x.embed) {
        let embed = x.embed[prop]
        //console.log('reset embed')
        embed.form.reset()

        if (embed.model) {
            embed.model.clear()
        } else {
            for (let modelKey in embed.multiModel) {
                embed.multiModel[modelKey].clear()
            }
        }
    }
}

Formobj.prototype.reset = function() {
    //console.log('reset:', this)

    this._reseting = true
    this.hideError()
    //this._form.reset()

    this._resetFields(this._tree)

    //console.log('ENABLE list:', this._disabledElems, this)
    //console.log(this._readonlyElems)
    
    if (this._submit) {
        this._submit.classList.remove(this._btnRemoveClass)

        if (this.isDisabled()) {
            this._submit.disabled = true        
        } else {
            this._setSubmitText(this._btnSubmitText)
            this._submit.disabled = this._isSubmitDisabledByDefault
        }
    }

    if (this._btnRemove) {
        this._btnRemove.disabled = this._isBtnRemoveDisabledByDefault
    }

    this._version++
    this._settingObj = null
    this._reseting = false
    //console.log('reseted form:', this._form)
}

Formobj.prototype._setObj = function(x) {
    let obj = this._settingObj.obj
    console.log('_setObj:', x, obj)
    //console.log('setSchemaFields:', this)
    // Быстрые ссылки на disabled элементы из schema. В список попадают только элементы
    // c disabled = false
    this._disabledElems = []
    this._readonlyElems = []

    //if (x.plain)
    for (let prop in x.schema) {
        let f = x.schema[prop] // field
        let elem = f.elem

        //console.log('set schema value:', prop, f, obj[prop])

        if (obj[prop] !== undefined) {
            let v = obj[prop]
            
            if (f.dataType == 'bool') {
                if (v) {
                    f.elem.checked = true
                } else {
                    f.elem.checked = false
                }
            } else {
                if (f.dataType == 'hex') {
                    if (v) {
                        v = base64ToHex(v)
                    }
                }

                if (f.set) {
                    elem.value = f.set(v)
                } else {
                    elem.value = v
                }
            }
        }

        if (this._isEditable) {
            switch (f.editmode) {
            case Formobj.DISABLED:
                elem.disabled = true
                this._disabledElems.push(elem)
                break

            case Formobj.READONLY:
                if (elem.tagName == 'SELECT') {
                    elem.disabled = true
                    this._disabledElems.push(elem)
                } else {
                    elem.readOnly = true
                    this._readonlyElems.push(elem)
                }
                break
            
            //case Formobj.EDITABLE:
                
                //  break
            }        
        } else {
            elem.disabled = true
            this._disabledElems.push(elem)
        }


        
        /*
        if (this.isDisabled()) {
            console.log('disable:', prop)
            if (!elem.disabled) {
                elem.disabled = true
                this._disabledElems.push(elem)
            }
        } else {
            console.log('not disabled:', prop, f)
            // WARNING! NEW FUTURE
            if (f.readonly) {
                if (elem.tagName == 'SELECT') {
                    elem.disabled = true
                    this._disabledElems.push(elem)
                } else {
                    elem.readOnly = true
                    this._readonlyElems.push(elem)
                }
            }
        }
        */
    }

    //console.log('DISABLED:', this._disabledElems)

    if (x.custom) {
        // только топ уровень
        for (let prop in x.custom) {
            let c = x.custom[prop]
                        
            console.log('custom:', c)
            //if (!c.manage) {
                //continue
            //}

            if (c.loading) {
                console.log('loading')
                continue
            }

            let nestedObj = obj[prop]

            //console.log('c:', c, 'nestedObj:', nestedObj, 'type:', c.elem.getType())

            switch (c.elem.getType()) {
            case 'select':
                
                if (c.key) {
                    //console.log('select (edit mode): ', nestedObj[c.key])
                    c.elem.select(nestedObj[c.key])
                } else {
                   // console.log('select (edit mode): ', nestedObj)
                    c.elem.select(nestedObj)
                }

                //console.log('selected:', c.elem.getSelected())
                //item.elem.select(nestedObj[item.key])
                break
                
            case 'list':
                c.elem.setValues(nestedObj)
                break;

            case 'input':
                if (c.key) {
                    c.elem.setValue(nestedObj[c.key])
                } else {
                    c.elem.setValue(nestedObj)
                }
                //item.elem.setValue(nestedObj)
                break;
            }

            if (this._isEditable) {
                if (c.editmode != Formobj.EDITABLE) {
                    c.elem.disable()
                }
            } else {
                c.elem.disable()
            }

            let t = c.tree
            if (t) {
                //console.log('tree:', t)
                if (t.current) {
                    this._setObj(t.current)
                }
            }
        }
    }

    if (x.embed) {
        for (let prop in x.embed) {
            let embed = x.embed[prop]

            if (embed.model) {
                embed.model.setList(obj[prop])
            } else {
                for (let modelKey in embed.multiModel) {
                    embed.multiModel[modelKey].setList(obj[modelKey])
                }
            }
        }
    }
}

// типа expand
Formobj.prototype.setObj = function(obj) {
    console.log('setObj:', obj, this)
    //if (!obj) {
    //console.log('caller:', arguments.callee.caller.toString())
    //console.trace()
    //}

    this.reset()

    if (!obj) {
        return
    }

    this._settingObj = {
        loadingCount: 0,
        obj: obj,
    }

    this._setObj(this._tree)

    //if (this._attachedObj) {
        //this._attachedObj.setObj(obj)
    //}

//    this._setSchemaFields(this._schema, obj)

    //for (let prop in this._embed) {
      //  this._embed[prop].model.setList(obj[prop])
    //}


    // /console.log('SETOBJ: isDisabled:', this.isDisabled())
    if (this.isDisabled()) {
        if (this._submit) {
            /*
            if (this._removeFunc) {
                this._setSubmitText(this._btnRemoveText)
                this._submit.classList.add(this._btnRemoveClass)
                this._submit.disabled = false
            }
            */
        }
        //this._submit.disabled = true // кнопка установлена в reset()
    } else {
        if (this._submit) {
            this._setSubmitText(this._btnUpdateText)
        }
        // Если форма целиком заполнена - считаем signature
        // Кнопку отключать не нужно, ибо в reset уже отключили
        this._onFormLoaded()
    }

    if (this._btnRemove) {
        this._btnRemove.disabled = false
    }
}

Formobj.prototype._setSubmitText = function(txt) {
    if (this._submit.tagName == 'INPUT') {
        this._submit.value = txt
    } else if (this._submit.tagName == 'BUTTON') {
        this._submit.textContent = txt
    }
}

/*
Сделать общую мапу для error элементов?

Ошибки:
- для plain элементов, 
- для custom элементов,
- для отдельного пункта в списке (списки простых значений допускаются только в custom элементах) 
- для модели в целом (например пустой список недопустим)

_fields
*/

Formobj.prototype.getElem = function(name) {
    let f = this._fields[name]
    if (!f) {
        throw new Error(`Unknown field ${name}`)
    }
    if (f.type == 'custom' || f.type == 'plain') {
        return f.elem
    }
    throw new Error(`Unknown field ${name}`)
}

Formobj.prototype.showError = function(err) {
    this.hideError()
    
    //console.log('showErr: ', err)
    //console.log('fields:', this._fields)
    if (Array.isArray(err.path) && err.path.length > 0) {
        /*
        let leg = err.path.shift()
        let embed = this._tree.embed[leg.field]
        if (!embed) {
            throw new Error(`Wrong leg: ${JSON.stringify(leg)}`)
        }
        if (embed.model.getSelected()) {
            embed.model.unselect()
        }
        embed.model.select(leg.idx)
        embed.form.showError(err)
        */
        let leg = err.path.shift()
        let f = this._fields[leg.field]
        if (!f || f.type != 'embed') {
            throw new Error(`Wrong leg: ${JSON.stringify(leg)}`)
        }
        if (f.model.getSelected()) {
            f.model.unselect()
        }
        f.model.select(leg.idx)
        f.form.showError(err)
    } else {
        let anchor;
        if (err.field) {
            let f = this._fields[err.field]
            if (!f) {
                throw new Error(`Error message for unknown field ${err.field}`)
            }

            switch (f.type) {
            case 'custom':
                anchor = f.elem.getAnchor()
                break;
            case 'plain':
                anchor = f.elem
                break;
            case 'embed':
                this._error = f.model.setError(err)
                return
                /*
                for (let prop in this._tree.embed) {
                    let embed = this._embed[prop]
                    if (embed.model.getSelected()) {
                        embed.model.unselect()
                    }
                }
                */
            default:
                throw new Error(`Unknown field type ${f.type}`)
            }
        } else if (err.list) {
            let f = this._fields[err.list]
            if (!f || f.type != "custom") {
                throw new Error(`Error message for unknown list ${err.list}`)
            }
            anchor = f.elem.getAnchor(err.idx)
        }
        
        if (anchor) {
            this._error = insertError({
                after: anchor, 
                err: err,
                class: this._errorClass
            })
        } else {
            // Если якорь не найден (поля field и list пустые) - ошибка относится к форме
            // целиком. Поэтому показываем перед submit кнопкой.
            this._error = insertError({
                before: this._submit, 
                err: err,
                class: this._errorClass
            })
        }
        // нужно ли ?
        /* // multiModel ???
        for (let prop in this._tree.embed) {
            let embed = this._tree.embed[prop]
            if (embed.model.getSelected()) {
                embed.model.unselect()
            }
        }
        */
    }
}


Formobj.prototype.hideError = function() {
    if (this._error) {
        this._error.remove()
    }
}

Formobj.prototype._onSubmit = function() {
    this.hideError()

    let obj = this.getFormData()

    if (this._validate) {
        let err = this._validate(obj)
        if (err) {
            this.showError(err)
            return
        }
    }

    //console.log('formData:', obj)

    // Кейсы:
    // 1. Модель загружается по api. Форма добавляет объект по api. Редактирования нет.
    // Привязка нужна для просмотра.
    // 2. Модель загружается по api. Форма добавляет объект по api, редактирует по api.
    // Привязка нужна для редактирования. Модель canUpdate = false, но непустой
    // _updateFunc позволит не отключать форму.
    // 3. Модель заполняется и редактируется из формы.
/*
    if (this._removeFunc) {
        // На модель не проверяем, ибо уже проверили в конструкторе
        let selected = this._model.getSelected()
        if (selected) {
            let id = selected[this._key]
            let p = {}
            p[this._key] = id

            this._submit.disabled = true

            this._api.req({
                "func": this._removeFunc,
                "data": p,
                "onError": err => {
                    this._submit.disabled = false
                    this.showError(err)
                },
                "onSuccess": resp => {
                    if (this._model) {
                        // Именно перезагружаем, чтобы избежать ошибок, если модель с параметром.
                        console.log('model reload, form reset (removeFunc)')
                        this._model.reload()
                        this.reset()
                    } else {
                        if (this._isSubmitDisabledByDefault) {
                            this._calcSignature()
                        }
                        this._setSubmitText(this._btnSubmitText)
                        this._submit.disabled = this._isSubmitDisabledByDefault
                    }
                    // FIX
                    if (this._userOnSubmit) {
                        this._userOnSubmit()
                    }
                }
            })
            return
        }        
    } 
    */
    
    if (this._getSubmitFunc) {
        let url = this._getSubmitFunc(obj)
        if (!url) {
            console.log(`getSubmitFunc returns: ${url}`)
            return
        }
        console.log('SUBMIT:', this.cleanObj(obj))
        this._submit.disabled = true
    
        this._api.req({
            "func": url,
            "data": this.cleanObj(obj),
            "onError": err => {
                this._submit.disabled = false
                this.showError(err)
            },
            "onSuccess": resp => {
                if (this._model) {
                    // Именно перезагружаем, чтобы избежать ошибок, если модель с параметром.
                    console.log('model reload, form reset (submitFunc)')
                    this._model.reload()
                    this.reset()
                //} else if (this._models) {
                //    this.reset()
                } else {
                    if (this._isSubmitDisabledByDefault) {
                        this._calcSignature()
                    }
                    this._setSubmitText(this._btnSubmitText)
                    this._submit.disabled = this._isSubmitDisabledByDefault
                }
                // FIX
                if (this._userOnSubmit) {
                    this._userOnSubmit()
                }
                if (this._onSubmitResult) {
                    this._onSubmitResult(resp)
                }
            }
        })
    } else if (this._model) {
        // без ajax
        if (this._model.getSelected()) {
            //console.log('UPDATE MODEL:', obj)
            this._model.updateSelected(obj)
            // Если обновление пройдет успешно - получим событие 'select' 
            // с пустым аргументом и форма сбросится автоматически
        } else {
            //console.log('ADD TO MODEL:', obj)
            let ok = this._model.add(obj)
            if (ok) {
                //console.log('ADDED.')
                console.log('after model add')
                this.reset()
            }
        }
    } else if (this._models) {
        // без ajax
        let modelKey = this._getModel(obj)
        let model = this._models[modelKey]

        if (!model) {
            throw new Error(`not found model for modelKey ${modelKey}`)
        }

        if (model.getSelected()) {
            //console.log('UPDATE MODEL:', obj)
            model.updateSelected(obj)
            // Если обновление пройдет успешно - получим событие 'select' 
            // с пустым аргументом и форма сбросится автоматически
        } else {
            //console.log('ADD TO MODEL:', obj)
            let ok = model.add(obj)
            if (ok) {
                //console.log('ADDED.')
                console.log('after model add')
                this.reset()
            }
        }
    } else if (this._userOnSubmit) {
        // без ajax, без модели
        this._userOnSubmit(obj)
    }
/*
    if (this._model) {
        if (this._model.getSelected()) {
            if (this._updateFunc) {
                this._sendUpdateReq(obj)
            } else {
                // без ajax
                this._model.updateSelected(obj)
                // Если обновление пройдет успешно - получим событие 'select' 
                // с пустым аргументом и форма сбросится автоматически
            }
        } else {
            if (this._submitFunc) {
                this._sendSubmitReq(obj)
            } else {
                // без ajax
                let ok = this._model.add(obj)
                if (ok) {
                    this.reset()
                }
            }
        }
    } else {
        if (this._submitFunc) {
            this._sendSubmitReq(obj)
        } else if (this._userOnSubmit) {
            // без ajax, без модели
            this._userOnSubmit(obj)
        }
    }
    */
}
/*
Formobj.prototype._sendUpdateReq = function(obj) {
    this._submit.disabled = true
    let x = this.cleanObj(obj)

    this._api.req({
        func: this._updateFunc,
        data: x,
        onError: err => this._onAjaxSubmitError(err),
        onSuccess: resp => this._onAjaxSubmitSuccess(resp)
    })
}

Formobj.prototype._sendSubmitReq = function(obj) {
    this._submit.disabled = true
    let x = this.cleanObj(obj)

    this._api.req({
        func: this._submitFunc,
        data: x,
        onError: err => this._onAjaxSubmitError(err),
        onSuccess: resp => this._onAjaxSubmitSuccess(resp)
    })
}

Formobj.prototype._onAjaxSubmitError = function(err) {
    this._submit.disabled = false
    this.showError(err)
}

Formobj.prototype._onAjaxSubmitSuccess = function(resp) {
    if (this._model) {
        // Именно перезагружаем, чтобы избежать ошибок, если модель с параметром.
        this._model.reload()
        this.reset()
    } else {
        if (this._isSubmitDisabledByDefault) {
            this._calcSignature()
        }
        this._setSubmitText(this._btnSubmitText)
        this._submit.disabled = this._isSubmitDisabledByDefault
    }
}
*/


Formobj.prototype.getCustomElemData = function(custom) {
    let obj = {}
    for (let prop in custom) {  

        let a = custom[prop]

        //console.log('getCustomElemData:', a)
        switch (a.elem.getType()) {
        case 'list':
            obj[prop] = a.elem.getValues()                
            break;

        case 'select':
            obj[prop] = a.elem.getSelected()
            break

        case 'input':
            obj[prop] = a.elem.getValue()
            //console.log('X:', prop, obj[prop])
            break
        }

        if (a.chains) {
            // fix добавить рекурсивный обход только custom
            obj = Object.assign(obj, this.getCustomElemData(a.chains))
        }

        if (a.tree) {
            let cur = a.tree.current
            if (cur) {
                //console.log('cur:', cur)
                obj = Object.assign(obj, this.getData(cur))
            }
        }
    }
    return obj
}


Formobj.prototype.getFormData = function() {
    let obj = {}

    for (let name in this._predefined) {
        obj[name] = this._predefined[name]
    }

    obj = Object.assign(obj, this.getData(this._tree))

    if (this._transformData) {
        obj = this._transformData(obj)
    }

    //console.log('getFOrmData:', JSON.stringify(obj))

    return obj
}

Formobj.prototype.getData = function(x) {
    //console.log('gteData x:', x)
    let obj = {}

    // SCHEMA FUTURE
    if (x.schema) {
        let fd = new FormData(this._form)

        // SCHEMA
        for (let prop in x.schema) {
            let dataType = x.schema[prop].dataType
            let value = fd.get(prop)
            let v
    
            // Обрезаем пробелы
            if (typeof value == 'string') {
                value = value.trim()
            }

            switch (dataType) {
            case 'int':
                v = parseInt(value) || 0
                break              
            case 'float':
                v = parseFloat(value) || 0
                break
            case 'bool':
                console.log('bool:', value)
                if (value == 'on') {
                //if ()
                    v = true
                } else {
                    v = false
                }
                break
            case 'str':
                v = value || ''
                break

            case 'hex':
                try {
                    v = hexToBytes(value)
                } catch (e) {
                    v = ''
                }
                break

            default:
                throw new Error(`Unknown type: ${dataType}`)
            }
            obj[prop] = v
        }
    }

    // fix как подружить объекты и схемы?

    // type, elem, chains, tree
    if (x.custom) {
        obj = Object.assign(obj, this.getCustomElemData(x.custom))
    }

    if (x.embed) {
        for (let prop in x.embed) {
            let embed = x.embed[prop]

            if (embed.model) {
                obj[prop] = embed.model.getList()
            } else if (embed.multiModel) {
                for (let modelKey in embed.multiModel) {
                    obj[modelKey] = embed.multiModel[modelKey].getList()
                }
            }
        }
    }

    return obj
}

Formobj.prototype._buildTree = function(opt) {
    let r = {
        //plain: opt.schema,
        custom: {},
        embed: opt.embed,
        // temp
        schema: {}
    }

    if (opt.schema) {
        for (let prop in opt.schema) {
            let v = opt.schema[prop]
            if (typeof v == 'string') {
                r.schema[prop] = {
                    dataType: v,
                    editmode: Formobj.EDITABLE
                }
            } else {
                let f = {
                    dataType: v.dataType,
                    get: v.get,
                    set: v.set,
                }

                if (v.editmode) {
                    switch (v.editmode) {
                    case Formobj.DISABLED:
                        f.editmode = Formobj.DISABLED
                        break
                    
                    case Formobj.READONLY:
                        f.editmode = Formobj.READONLY
                        break
                    
                    case Formobj.EDITABLE:
                        f.editmode = Formobj.EDITABLE
                        break
            
                    default:
                        throw new Error(`Unknown '${prop}' editmode: ${v.editmode}`)
                    }
                } else {
                    f.editmode = Formobj.EDITABLE
                }

                r.schema[prop] = f
            }
        }
    }

    //console.log('buildTree:', opt)

    if (opt.attached) {
        for (let prop in opt.attached) {
            let c = opt.attached[prop]

            r.custom[prop] = this._buildCustom(c, prop)
        }
    }

    //console.log('returnned tree:', opt, r)
    return r
}

Formobj.prototype._buildCustom = function(c, prop) {
    if (!c.elemConstructor) {
        throw new Error(`'elemConstructor' option is required for custom element ${prop}`)
    }    

    let f = {
        prop: prop,
        setProp: c.setProp,
        setKey: c.setKey,
        //readonly: c.readonly,
        virtual: c.virtual, // вирутальное поле, например RadioList, в form.elements не проверять
        elemConstructor: c.elemConstructor,
        opts: c.opts,
        //func: c.func,
        //getFunc: c.getFunc,
        key: c.key,
        manage: true,
        eventHandler: c.eventHandler,
        cache: new Map(),
        requestModifier: c.requestModifier,
        responseModifier: c.responseModifier,

        chains: c.chains || {}
    }

    if (c.editmode) {
        switch (c.editmode) {
        case Formobj.DISABLED:
            f.editmode = Formobj.DISABLED
            break
        
        case Formobj.READONLY:
            f.editmode = Formobj.READONLY
            break
        
        case Formobj.EDITABLE:
            f.editmode = Formobj.EDITABLE
            break

        default:
            throw new Error(`Unknown '${prop}' editmode: ${c.editmode}`)
        }
    } else {
        f.editmode = Formobj.EDITABLE
    }

    if (c.func) {
        f.getFunc = function() {
            return c.func
        }
    } else if (c.getFunc) {
        f.getFunc = c.getFunc
    }

    if ((f.setProp && !f.setKey) || (!f.setProp && f.setKey)) {
        throw new Error("'setKey' and 'setProp' options must be set or not set both")
    }

    //console.log('opt/f', opt, f)

    if (c.div) {
        f.div = getElemSafe('div', c.div)
    }

    if (c.manage === false) {
        f.manage = false
    }
    // создаем экземпляр кастомного элемента для проверки
    //let elem = new c.elem(c.opts)
    // для правильного считывания/установки значения
    //c.type = elem.type

    // tree
    let t = c.tree
    if (t) {
        let branches = new Map()
        let x = {
            getValue: t.getValue,
            container: t.container,
            branches: branches,
        }

        t.cases.forEach(b => {
            let branch = {
                templateId: b.templateId,
                templates: b.templates,
                value: b.value,
                values: b.values,
            }

            let tmp = this._buildTree(b)
            branch = Object.assign(branch, tmp)

            if (b.value) {
                branches.set(branch.value, branch)
            } else if (Array.isArray(b.values)) {
                b.values.forEach(value => {
                    branches.set(value, branch)
                })
            } else {
                throw new Error(`'value' or 'values' option required in branch settings`)
            }
        })
        
        f.tree = x
    }

    for (let prop in c.chains) {
        let d = c.chains[prop]

        f.chains[prop] = this._buildCustom(d, prop)
    }
    return f
}

Formobj.prototype.cleanObj = function(obj) {
    //console.log('cleanData:', JSON.stringify(obj))
    let r = {}
    this._cleanData(r, this._tree, obj)   
    
    //console.log('after cleanData:', JSON.stringify(r))

    if (this._transformData) {
        r = this._transformData(r)
    }
    //console.log('getCleanData return:', r)
    return r
}

// r - result object, a - custom object
Formobj.prototype._cleanCustomElem = function(r, a, obj) { //, prop) {
    //r.xxxx = 1900000
    //console.log('CLEAN ATTACHED:', a)
        //console.log('CLEAN ATTACHED obj:', obj)

    let valueObj = obj[a.prop]
    if (valueObj !== undefined) {
        if (a.key) {        
            // копируем только ID
            
            // Объекта может не быть, если юзер ничего не выбрал (например, в выпадающем списке)
            
            r[a.key] = valueObj[a.key]     
    //            console.log('REPLACED:', a.key, id, 'DELETED', prop)
        } else {
            //console.log('DELETED', prop)
            r[a.prop] = valueObj
        }
    }

    if (a.chains) {
        for (let prop in a.chains) {
            this._cleanCustomElem(r, a.chains[prop], obj)
            //console.log('_CLEAN CUSTOM: AFTER CLEAN ATTACHED:', JSON.stringify(r))
        }
    }

    let tree = a.tree
    if (tree) {
        var v;
        if (tree.getValue) {
            v = tree.getValue(valueObj) // obj
        } else {
            if (a.key) {
                v = r[a.key]
            } else {
                v = valueObj
            }
        }

        let branch = tree.branches.get(v)
        //console.log('v:', v, 'branch:', branch)
        if (branch) {
            if (branch.custom) {
                r = this._cleanData(r, branch, obj)
            }
        }
    }


    //console.log('CLEAN ATTACHED:', JSON.stringify(r))
}

Formobj.prototype._cleanData = function(r, x, obj) {
    /*
    SCHEMA FUTURE
    if (x.plain) {
        for (let prop in x.plain) {
            r[prop] = obj[prop]
        }
    }
    */
    if (x.schema) {
        for (let prop in x.schema) {
            r[prop] = obj[prop]
        }
    }

    if (x.custom) {
        for (let prop in x.custom) {
            //let a = x.custom[prop]

            this._cleanCustomElem(r, x.custom[prop], obj) //, prop)

            //console.log('_cleanDATA: AFTER CLEAN ATTACHED:', JSON.stringify(r), a)
        }
    }

    if (x.embed) {
        for (let prop in x.embed) {
            
            let embed = x.embed[prop]

            console.log('_cleanData embed:', embed)
            if (embed.model) {
                //console.log('e:', e)
                let dirtyList = obj[prop]

                //console.log('dirtyList:', dirtyList)

                let cleanItems = []

                if (Array.isArray(dirtyList)) {
                    dirtyList.forEach(dirtyItem => {
                        let cleanItem = embed.form.cleanObj(dirtyItem)
                        cleanItems.push(cleanItem)
                    })
                }

                r[prop] = cleanItems
            } else if (embed.multiModel) {
                for (let modelKey in embed.multiModel) {
                    let dirtyList = obj[modelKey]

                    //console.log('dirtyList:', dirtyList)

                    let cleanItems = []

                    if (Array.isArray(dirtyList)) {
                        dirtyList.forEach(dirtyItem => {
                            let cleanItem = embed.form.cleanObj(dirtyItem)
                            cleanItems.push(cleanItem)
                        })
                    }

                    r[modelKey] = cleanItems
                }
            } else {
                //throw new Error(`bug: embed doesn't contain 'model' or 'multiModel' option`)
            }
        }
    }
}

Formobj.prototype._createCustomElemsOnly = function(customElems) {
    if (customElems) {
        for (let prop in customElems) {
            let c = customElems[prop]

            let opts = Object.assign({}, c.opts)

            // для удобного отображения ошибок
            let name = prop
            if (c.key) {
                name = c.key
            }

            if (!c.virtual) {
                let field = this._form.elements[name]
                if (!field) {
                    throw new Error(`Field '${name}' not found`)
                }
                
                opts.field = field
            }
            
            //if (c.type != 'select' && c.type != 'list' && item.type != 'input') {
            //    throw new Error(`Unknown attached elem type ${item.type}. Valid types: select, list, input`)
            //}
            // создаем экземпляр кастомного элемента


            c.elem = new c.elemConstructor(opts)


            this._fields[name] = {
                type: 'custom',
                elem: c.elem
            }

            if (this._isSubmitDisabledByDefault) {
                switch (c.elem.getType()) {
                case 'select':
                    // Select, RadioList
                    c.elem.on('select', () => this._onAnyChange())
                    break;
        
                case 'list':
                    // InputList, CheckList
                    c.elem.on('changed', () => this._onAnyChange())
                    break;
                }
            }

            //if (c.func) {
              //  console.log(this, 'loadData')
                //this._loadData(c)
            //}

            /*
            Пофиксить
            if (item.key) {
                this._attachedElems[item.key] = item.elem
            } else {
                this._attachedElems[prop] = item.elem
            }
            */
            // для правильного считывания/установки значения
            //c.type = elem.type
            

            if (c.tree || c.chains) {
                c.event = c.elem.getActionType()
                if (c.event != 'select' && c.event != 'input') {
                    throw new Error(`Custom element '${prop}' has wrong action type '${c.event}'`)
                }
                // fix
                c.elem.on(c.event, (id) => this._onElemEvent(id, c))
            }
            // объекты из chains создаем сразу, а заполняем (и, по желанию, 
            // скрываем/показываем) в ответ на событие
            if (c.chains) {
                // создаем дальше, chains - тоже map
                this._createCustomElemsOnly(c.chains)
            }
            // объекты из Tree создаются только в ответ на событие                        
        }
    }
}

Formobj.prototype._createObjects = function(opt) {
    
    if (opt.schema) {
        for (let prop in opt.schema) {
            let f = opt.schema[prop]
            let elem = this._form.elements[prop]
            if (!elem) {
                throw new Error(`Field '${prop}' not found`)
            }
            if (!(elem instanceof Element)) {
                //throw new Error(`Form element '${prop}' not found`)
                throw new Error(`Field ${prop} must be instanceof Element. Not ${Object.prototype.toString.call(elem)}`)
            }
            f.elem = elem
            f.defaultValue = elem.value
            // для удобного отображения ошибок
            this._fields[prop] = {
                type: 'plain',
                elem: elem
            }

            if (f.dataType == 'bool') {
                if (elem.nodeName != 'INPUT') {
                    throw new Error('bool elem must be checkbox')
                }
                if (elem.type != 'checkbox') {
                    throw new Error('bool elem must be checkbox')
                }
                //elem.value = ''
            }

            // Ловим события изменения
            if (this._isSubmitDisabledByDefault) {
                let eventName = 'input' // для input, textarea
                // Чтобы избежать багов в разных браузерах - для select, checkbox и
                // radio - повесим обработчик на другое событие - 'change'
                if (elem.nodeName == 'SELECT') {
                    eventName = 'change'
                } else if (elem.nodeName == 'INPUT') {
                    if (elem.type == 'radio' || elem.type == 'checkbox') {
                        eventName = 'change'
                    }
                }
                elem.addEventListener(eventName, () => this._onAnyChange())
            }
        }
    }
    
    /*
    // SCHEMA FUTURE
    opt.plainElems = {}
    if (opt.plain) {
        for (let prop in opt.plain) {
            let elem = this._form.elements[prop]
            if (!elem) {
                throw new Error(`Field '${prop}' not found`)
            }
            if (!(elem instanceof Element)) {
                //throw new Error(`Form element '${prop}' not found`)
                throw new Error(`Field ${prop} must be instanceof Element. Not ${Object.prototype.toString.call(elem)}`)
            }
            opt.plainElems[prop] = elem
            // для удобного отображения ошибок
            this._fields[prop] = {
                type: 'plain',
                elem: elem
            }

            // Ловим события изменения
            if (this._isSubmitDisabledByDefault) {
                let eventName = 'input' // для input, textarea
                // Чтобы избежать багов в разных браузерах - для select, checkbox и
                // radio - повесим обработчик на другое событие - 'change'
                if (elem.nodeName == 'SELECT') {
                    eventName = 'change'
                } else if (elem.nodeName == 'INPUT') {
                    if (elem.type == 'radio' || elem.type == 'checkbox') {
                        eventName = 'change'
                    }
                }
                elem.addEventListener(eventName, () => this._onAnyChange())
            }
        }
    }
    */

    if (opt.custom) {
        this._createCustomElemsOnly(opt.custom)
    }

    for (let prop in opt.embed) {
        
        //console.log('listenChanges:', this._listenChanges)

        let e = opt.embed[prop]

        if (e.modelOpts) {
            let model = new ListModel(e.modelOpts)

            let formOpts = Object.assign({}, e.formOpts)
            formOpts.model = model
            formOpts.parent = this

            let form = new Formobj(formOpts)
            //form.setIsDisabledFunc(this.isDisabled)
            model.setIsDisabledFunc(this.isDisabled.bind(this))

            //embed[prop] = {
            //    model: model,
            //    form: form
            //}
            e.model = model
            e.form = form

            // для удобного отображения ошибок
            this._fields[prop] = {
                type: 'embed',
                model: model,
                form: form
            }


            if (this._isSubmitDisabledByDefault) {
                model.on('listChanged', () => {
                    //console.log('listChanged -> on any change')
                    this._onAnyChange()
                })
            }
        } else if (e.multiModelOpts) {
            let models = {}

            for (let modelKey in e.multiModelOpts) {
                let modelOpts = e.multiModelOpts[modelKey]

                let model = new ListModel(modelOpts)
                model.setIsDisabledFunc(this.isDisabled.bind(this))

                if (this._isSubmitDisabledByDefault) {
                    model.on('listChanged', () => {
                        //console.log('listChanged -> on any change')
                        this._onAnyChange()
                    })
                }

                models[modelKey] = model
            }

            let formOpts = Object.assign({}, e.formOpts)
            formOpts.models = models
            formOpts.parent = this

            let form = new Formobj(formOpts)
            //form.setIsDisabledFunc(this.isDisabled)
            

            for (let modelKey in models) {
                let model = models[modelKey]
                
                // для удобного отображения ошибок
                this._fields[modelKey] = {
                    type: 'embed',
                    model: model,
                    form: form
                }
            }
            e.multiModel = models
            e.form = form            
        } else {
            throw new Error(`embed requires 'modelOpts' or 'multiModelOpts'`)
        }
    }

    //this._onAnyChange()
    //this._calcSignature()
    

    //console.log('_createObjects:', opt)
    // Загружаем все AJAX-элементы топ уровня (которые ни от кого не зависят)
    // fix
    /*
    this._chains.forEach(c => {
        if (!c.func) {
            return
        }
        this._loadData(item)
    })
*/
}

Formobj.prototype._onElemEvent = function(id, c) {
    console.log('elem event:', id, c)

    let p;
    switch (c.elem.getType()) {
    case 'list':
        p = c.elem.getValues()                
        break;

    case 'select':
        p = c.elem.getSelected()
        break

    case 'input':
        p = c.elem.getValue()
        //console.log('X:', prop, obj[prop])
        break
    }

    if (c.eventHandler) {
        if (!c.manage && this._settingObj) {
            // pass
        } else {
            c.eventHandler(p)
        }
    }
    
    if (c.chains) {
        for (let prop in c.chains) {
            this._clearDepended(c.chains[prop]) // fix elem param?
        }

        if (id) {
            for (let prop in c.chains) {
                let d = c.chains[prop] // dependend
                //console.log('chain item:', d)
            /*
            // fix цикл по chains
            if (item.doFunc) {                    
                let data;
                switch (c.event) {
                case 'select':
                    data = elem.getSelected()
                    break

                case 'input':
                    data = elem.getValue()
                    break
                }
                // что-нибудь типа setList для InputList, либо buildAddrSelect для addr                    
                c.doFunc(data)
                //console.log('SETvALUE after doFunc')
                this._setValueIfEditMode(c) // fix elem param?
            } else {
                */
                // Если данные есть в кэше - достаем из кэша
                //let m = this._cache[item.prop]
                //let list = m.get(id)
                let cleanParam = id
                if (d.requestModifier) {
                    cleanParam = d.requestModifier(p)
                }

                //console.log('d:', d)
                //console.log('p:', p)
                //console.log('id:', id)
                //console.log('cleanParam:', cleanParam)

                let list = d.cache.get(cleanParam)
                if (list) {
                    d.elem.setList(list)
                    this._setValueIfEditMode(d) // fix elem param?
                } else {
                    this._loadData(d, p, cleanParam) // fix elem param?
                }
            }
            //}

            //if (c.div) {
            //    c.div.hidden = false
            //}
        }
    }

    let t = c.tree
    if (t) {
        let cleanId = id
        if (t.getValue) {
            cleanId = t.getValue(p)
        }               

        let branch = t.branches.get(cleanId)

        //console.log(id)
        console.log('branch:', branch)
        console.log(t)
        //console.log('container:', t.container)

        let container = document.getElementById(t.container)
        if (!container) {
            throw new Error(`container '${t.container}' not found ('${t.prop}' tree)`)
        }

        if (!branch) {
            if (container) {
                container.innerHTML = ''
            }
            
            if (t.containers) {
                t.containers.forEach(containerId => {
                    document.getElementById(containerId).innerHTML = ''
                })
                t.containers = null
            }
            t.current = undefined

            // fix удалить elems в текущем и дочерних элементах
            //console.log('t:', t)
        } else {
            if (t.containers) {
                t.containers.forEach(containerId => {
                    document.getElementById(containerId).innerHTML = ''
                })
                t.containers = null
            }
            // Шаблон необязателен. Пример: один объект состоит только из полей по умолчанию
            // Второй объект требует дополнительных полей (например для COM-порта нужны
            // наcтройки интерфейса RS485)
            if (branch.templateId) {
                copyTemplateInto(branch.templateId, container)

                this._createObjects(branch)
            } else if (branch.templates) {
                t.containers = []
                for (let container in branch.templates) {
                    let templateId = branch.templates[container]

                    if (templateId != '') {
                        copyTemplateInto(templateId, container)
                        t.containers.push(container) // id
                    } else {
                        document.getElementById(container).innerHTML = ''
                    }

                    
                }
                this._createObjects(branch)
            }

            t.current = branch

            for (let prop in branch.custom) {
                let c = branch.custom[prop]
                if (c.getFunc) {
                    let cleanParam = id
                    if (c.requestModifier) {
                        cleanParam = c.requestModifier(p)
                    }
//console.log('id:', id, 'p:', p)

                    this._loadData(c, p, cleanParam) // fix
                }
            }
        }
    }

    if (c.div) {
        if (id) {
            c.div.hidden = false
        } else {
            c.div.hidden = true
        }
    }
}


Formobj.prototype._clearDepended = function(c) {
    c.elem.clear()

    if (c.div) {
        c.div.hidden = true
    }
    // FIX if item.after ?
    if (c.divLoadError) {
        c.divLoadError.remove()
    }
    
    if (c.chains) {
        for (let prop in c.chains) {
            this._clearDepended(c.chains[prop])
        }
    }
}

// hex fields


function base64Decode(text){
    if (!text) {
        return []
    }

    text = text.replace(/\s/g,"");

    if(!(/^[a-z0-9\+\/\s]+\={0,2}$/i.test(text)) || text.length % 4 > 0){
        throw new Error("Not a base64-encoded string.");
    }   

    //local variables
    var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
        cur, prev, digitNum,
        i=0,
        result = [];

    text = text.replace(/=/g, "");

    while(i < text.length){

        cur = digits.indexOf(text.charAt(i));
        digitNum = i % 4;

        switch(digitNum){

            //case 0: first digit - do nothing, not enough info to work with

            case 1: //second digit
                result.push(prev << 2 | cur >> 4);
                break;

            case 2: //third digit
                result.push((prev & 0x0f) << 4 | cur >> 2);
                break;

            case 3: //fourth digit
                result.push((prev & 3) << 6 | cur);
                break;
        }

        prev = cur;
        i++;
    }

    return result;
}

function base64ToHex(base64Str) {
    let buf = base64Decode(base64Str)
    let hexes = []
    buf.forEach(b => {
        let hex = b.toString(16)
        if (hex.length == 1) {
            hex = '0' + hex
        }
        hexes.push(hex)
    })
    return hexes.join(' ')
}

// принимает строку с опциональными пробелами между байтами
function hexToBytes(str) {
    let bytes = []
    let arr = str.split(' ')
    arr.forEach(elem => {
        if (elem == '') {
            return
        }
        if (elem.length % 2) {
            throw new Error("Wrong hex string")
        }
        // секции вида: 0c, ddff
        //console.log('elem:', elem)
        for (let i=0; i<elem.length; i+=2) {
            let hex = elem.substr(i, 2)
            //console.log('i:', i, 'hex:', hex)
            let num = parseInt(hex, 16)
            if (isNaN(num)) {
                throw new Error("Wrong hex string")
            }
            bytes.push(num)
        }        
    })
    return bytes
}