/*
Начальный расчет signature делаем после загрузки главной формы.
Как узнать когда загрузилась главная форма? 
- Заводим для формы счетчик loadingCount, 
увеличиваем его при создании элемента с функцией loadFunc, уменьшаем при заврешении 
http-запроса loadFunc (с ошибкой или без). Уменьшив счетчик проверяем - если 0 - форма
загрузилась.
Таким же образом рассчитываем начальный signature после restore / edit.

Как правильно отобразить ошибку, если юзер удалил ошибочные элементы после submit формы 
на сервер но до получения ответа от сервера?
- Перед отправкой на сервер clean объекта, нужно сохранить dirty объект. 
Если сервер вернул ошибку - делаем restore(dirty) и затем showError(err)

Все элементы формы генерируют события когда юзер что-то набирает в текстовом поле либо
выбирает в списке. Обработчик события извлекает 2 значения: clean и dirty. Затем 
обновляет цепочку объектов до топ-уровня (обновляя _clean и _dirty объекты). 
Затем (!) если есть зависимые элементы (fork, chains) - генерирует последующие события. 

_clean - это объект, который будет отправлен на сервер, а также выступает в роли 
текущей signature.

_dirty - это объект который нужен для восстановления (restore) состояния формы. 
Записывается в sessionStorage / localStorage для восстановления формы после перезагрузки
страницы. Снимок состояния в момент submit формы на сервер. Если сервер вернет ошибку - 
можно восстановить форму и корректно показать ошибку несмотря на изменения, внесенные 
юзером во время выполнения http-запроса.

Методы:

fill - заполняет поля формы будто это делает человек, не меняя режим filling. Зачем нужен этот метод? 

// Представим ситуацию: есть кликабельный список модемов, у каждого модема есть список приборов, у каждого прибора есть список метрик. На странице модема 

edit - заполняет поля формы, устанавливает режим в editing, чтобы можно было отключить (disabled=true) нередактируемые поля формы, а также рассчитывает initSignature после заполнения всех полей.

restore - восстанавливает состояние формы из снимка (snapshot)

Snapshot
Снимок формы хранит - dirty объект, initSignature 

*/


/*
Разделение на getObj и cleanObj.
getObj - возвращает объект в том виде, в котором можно вызвать setObj, то есть device вместо
deviceID, queries вместо queryIDs.

Если имеем многоуровневую форму и после setObj вызываем getObj, единственный способ получить
"чистый" объект для отправки на сервер - это сделать clean операцию на уже готовом объекте.


Список объектов для Formobj.

Одноуровневый список. Описываем в schema, создаем под капотом.


Топ-уровень - это модель, а вложенные списки объектов - это ObjectList?

Операции:
- add, с проверкой fingerprint
- replaceSelected
- removeChecked
- removeSelected

События:
- select (если параметр undefined - это unselect)

*/


/*
Модель (многоуровневая) для отображения данных из БД (или сервера).

У каждого объекта должен быть первичный ключ

- updateSelected, когда сервер присылает только некоторые поля объекта, например изменившийся статус
- replaceSelected


///////////////// setObj
заводим state object, который хранит obj и количество загружаемых элементов.
В конце setObj проверяем счетчик - если > 0 блокируем форму.
После загрузки успешной или нет - счетчик уменьшаем. Как только стал в 0 - разблокируем форму.


3 кейса для вложенных форм:
1. раскрывается под списком
2. в отдельном диве (например справа). Нужно передать ID блока
3. кастомная реализация. Нужно объявить 2 метода - showForm и hideForm


Разделить createForm для главной и вложенных. Другие кнопки, другие обработчики


DISABLED в createSchemaObjects


Для вложенных форм isEditable вместо editmode
*/


const filling = 0
const restoring = 1
const editing = 2

var _labelClass = 'omg-form-label' 

function Form(opt) {
    //this.formId = opt.form
    this._mode = filling
    this._loadingCount = 0
    this._version = 0
    
    this._enableSubmit = opt.enableSubmit

    this._api = opt.api
    this._errorClass = opt.errorClass || 'errmsg'

    this._key = opt.key

    this._context = opt.context

    this._btnSubmitText = opt.btnSubmitText || 'Добавить'
    this._btnUpdateText = opt.btnUpdateText || 'Обновить'
    this._btnRemoveText = opt.btnRemoveText || 'Удалить'

    //this._onSubmit = opt.onSubmit
    //this._onSubmitSuccess = opt.onSubmitSuccess
    //this._onUpdateSuccess = opt.onUpdateSuccess
    //this._onRemoveSuccess = opt.onRemoveSuccess
    //this._submitFunc = opt.submitFunc
    //this._updateFunc = opt.updateFunc || this._submitFunc
    //this._removeFunc = opt.removeFunc

    this._validate = opt.validate
    //this._form = document.getElementById(opt.form)
    //if (!this._form) {
    //    throw new Error(`html form '${opt.form}' not found`)
    //}
    //this._schema = {}
    //this._lists = {}

    this.form

    // temp
    this._fields = {}

    if (!opt.formContainer) {
        throw new Error(`formContainer option is required`)
    }

    if (opt.formContainer instanceof Element) {
        this._formContainer = opt.formContainer
    } else {
        let formContainer = document.getElementById(opt.formContainer)
        if (!formContainer) {
            throw new Error(`form container '${opt.formContainer}' not found`)
        }
    
        this._formContainer = formContainer
    }
    

    let form = {
        isEditable: opt.isEditable || false,
        schema: this._schema,
        fields: {}, // для ошибок,
        //loadingCount: 0
        selected: {},
        dirty: {}
    }

    //this._schema = this._buildTree(opt.schema, opt.form)
    this._schema = this._buildTree(opt.schema, form)

    this.form = form
    this.form.schema = this._schema

    //console.log('SCHEMA:', this._schema)
    
    //this._createSchemaObjects(this._schema)
    




    // fix - return ?
    //this._createMainForm(this._schema, formContainer, opt.btnSubmitText)

    // add submit btn to top form

    //console.log(this._schema)
    //this._loadDataForCustomElems(this._schema)
    
    /*
    let str = localStorage.getItem('x')
    if (str) {
        let s
        try {
            s = JSON.parse(str)
        } catch (e) {
            console.log(e)
            console.log(str)
        }
        console.log('localStore:', s.obj)

        this._restore(s)
    }
    */
    //console.log(this)
    //this.init()
}




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

Form.EDITABLE = 1 // стандартное поле
Form.UNEDITABLE = 2 // активно при добавлении, отключено при редактировании
Form.READONLY = 3 // всегда отключено, например ID, передается на сервер

/*
Form.E = 1 // editable
Form.R = 2 // readonly
Form.D = 3 // disabled

Form.S = 4 // standard
Form.C = 5 // custom
Form.L = 6 // list
*/

Form.prototype.init = function() {
    let loader = new Loader({
        api: this._api,
        t: this.form, // пустой dirty
        context: this._context,
        onError: err => {
            //console.log('Loader.err: ', err)
        },
        onLoaded:() => {
            this._createMainForm()            
        }
    })

    loader.load()
}

Form.prototype.onRemove = function(func) {
    this._onRemove = func
}

Form.prototype.onSubmit = function(func) {
    this._onSubmit = func
}

// ВАЖНО!
// Вложенные коллекции (в chains или tree) рекурсивно искать сложно, поэтому 
// в момент создания объектов нужно сохранить на них ссылки, а в момент reset - удалить.
Form.prototype.showError = function(err) {
    this._showError(this.form.fields, err)
}

/////////////// FIX
// сделать map из форм
Form.prototype._showError = function(fields, err) {
    if (this._error) {
        this._error.remove()
    }

    let path = []
    if (Array.isArray(err.path)) {
        path = err.path
    }

   
    while (true) {
        let step = path.shift()
        if (!step) {
            break
        }

        //if (!schema) {
        //    throw new Error(`wrong path step ${JSON.stringify(step)}, not found`) 
        //}

        let p = fields[step.field]

        /*
        schema.forEach(x => {
            if (x.name == step.field) {
                p = x
            }
        })
        */

        if (!p || p.type != 'list') {
            throw new Error(`wrong path step ${JSON.stringify(step)}`)
        }

        if (p.arr.length <= step.idx) {
            throw new Error(`wrong path step ${JSON.stringify(step)}, idx out of range`)
        }
        
        if (!p.isFormOpened) {
            this._select(p, p.arr[step.idx])
        }

        fields = p.fields
    }

    let anchor; //, found = false

    if (err.field) {
        //schema.forEach(p => {
        //    if (p.name == err.field) {
        let p = fields[err.field]

        //console.log('ERR FIELD:', p)

        if (p) {
            switch (p.type) {
            case '':
                // для скрытых элементов anchor не определяем
                break

            case 'line':
            case 'text':
            case 'select':            
                anchor = p.elem
                break
    
            case 'checkbox':
                anchor = p.labelElem
                break 

            case 'checkboxes':
            case 'radios':
            case 'model':
                anchor = p.cover
                break

            case 'list':
                anchor = p.btnDiv
                break
            }
        } else {  
            throw new Error(`field '${err.field}' not found`)
        }
        
    } else {
        // FIX
        //let p = schema[err.list]

        //anchor = p.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._btnSubmit, 
            err: err,
            class: this._errorClass
        })
    }
}


Form.prototype._buildTree = function(schemaConfig, form) {
    let schema = []

    if (Array.isArray(schemaConfig)) {
        schemaConfig.forEach(v => {
            let f;
            if (v.type == 'list') {
                f = this._buildList(v, form)                
            } else {
                f = this._buildField(v, form)                
            }
            schema.push(f)
        })
    }    

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

// Цель build функций - пройтись ко конфигурации схемы на всю глубину, провалидировать
// описание каждого свойства формы, создать внутренние структуры, описывающие свойства
// формы. 
// Никаких манипуляций с DOM данные функции не производят. Для fork случаев создание
// schema элементов с нуля после действий пользователя.
//Form.prototype._buildTree = function(schemaConfig, formId) {
Form.prototype._buildField = function(v, form) {
    let type = v.type
    if (!type) {
        type = ''
    }

    let f = {
        type: type,
        label: v.label,
        comment: v.comment,
        placeholder: v.placeholder,
        dataType: v.dataType || 'str',
        name: v.name,
        key: v.key,
        scalar: v.scalar,
        get: v.get,
        set: v.set,
        data: v.data,
        getData: v.getData,
        getName: v.getName, // fix
        loadFunc: v.loadFunc,
        loadFuncContextParam: v.loadFuncContextParam,
        chains: [],
        virtual: v.virtual, // вирутальное поле, например RadioList, в form.elements не проверять
        manage: true,
        onChange: v.onChange,
        cache: new Map(),
        getLoadFuncArg: v.getLoadFuncArg,
        //requestModifier: v.requestModifier,
        responseModifier: v.responseModifier,
        form: form,
        modelOptions: v.modelOptions,
        modelType: v.modelType,
        modelChangeType: v.modelChangeType,
        modelSelectType: v.modelSelectType,
        contextVar: v.contextVar,
        width: v.width,
        //height: v.height,
        rows: v.rows,
        cols: v.cols,
    }

    if (v.noCache === true) {
        f.caching = false
    } else {
        f.caching = true
    }
    // FIX
    /*
    if (v.key) {
        f.key = v.key
    } else {
        f.scalar = true
    }
    */

    //console.log('build:', f)

    if (v.getName) {
        if (typeof v.getName == 'string') {
            f.getName = obj => {
                return obj[v.getName]
            }
        } else {
            f.getName = v.getName
        }
    } else {
        f.getName = obj => obj
    }

    /*
    if (v.container) {
        f.containerId = v.container
    } else {
        f.containerId = formContainerId
    }
    */

    if (v.editmode) {
        // Если указан editmode - сперва проверяем значение на валидность
        if (v.editmode != Form.UNEDITABLE && v.editmode != Form.EDITABLE && v.editmode != Form.READONLY) {
            throw new Error(`Unknown '${v.name}' editmode: ${v.editmode}`)
        }
        // Если поле readonly - неважно какая форма - поле всегда отключено.
        if (v.editmode == Form.READONLY) {
            f.editmode = Form.READONLY
        } else {
            // Если форма редактируемая - поле может быть 2 видов
            if (form.isEditable) {
                f.editmode = v.editmode // editable / uneditable                
            } else {
                // Если форма нередактируемая - поле только uneditable
                f.editmode = Form.UNEDITABLE
            }
        }        
    } else {
        if (form.isEditable) {
            f.editmode = Form.EDITABLE
        } else {
            f.editmode = Form.UNEDITABLE
        }
    }

    switch (type) {
    case '':
    case 'checkboxes':
    case 'radios':
    case 'select':
    case 'line':
    case 'text':
    case 'checkbox':
    //case 'custom-list':
    case 'model':
        break

    default:
        throw new Error(`unknown type in '${v.name}' schema property`)
    }
    
    if (Array.isArray(v.chains)) {
        v.chains.forEach(x => {
            //console.log('CHAINS: =====>', JSON.stringify(x))
            let field = this._buildField(x, form)

            f.chains.push(field)
            //let y = this._buildField(x)
            //console.log()
        })
    }
    
    if (v.fork) {
        f.fork = this._buildFork(v.fork, form)
    }
    
    return f
}

// c - config item, k - property name (key)
// form - родительская форма - нужня в первую очередь для проверки isEditable
Form.prototype._buildList = function(c, form) {
    //if (!c.btnSubmit) {
      //  throw new Error(`list elem '${c.name}' must have 'btnSubmit' option`)
    //}
    let f = {
        type: 'list',
        label: c.label,
        name: c.name,
        isEditable: c.isEditable || false,
        //containerId: c.container,
        //btnSubmitId: c.btnSubmit,
        //btnHideId: c.btnHide,
        //formOpts: c.form,
        //templateOpts: c.template,
        //view: c.view,
        activeClass: c.activeClass || 'selected',
        //sort: c.sort,
        validate: c.validate,
        //parentFormId: formId,
        //formId: c.form,
        showForm: c.showForm,
        hideForm: c.hideForm,
        formContainerClass: c.formContainerClass,
        //btnAddText: c.btnAddText,
        //btnUpdateText: c.btnUpdateText,
        //formContainer: document.getElementById(c.formContainer),
        emptyListMsg: c.emptyListMsg || '',
        form: form,
        fields: {},
        loadingCount: 0
    }

    // Если родительская форма нередактируемая, то все вложенные тоже
    if (!form.isEditable) {
        f.isEditable = false
    }

    if (!c.getItemName) {
        throw new Error(`getItemName option is required`)
    }

    if (typeof c.getItemName == 'string') {
        f.getItemName = (obj) => {
            return obj[c.getItemName]
        }
    } else {
        f.getItemName = c.getItemName
    }

    // для всех schema элементов и ниже
    // FIX
    //if (!c.formContainer) {
    //    throw new Error(`formContainer option is required for list ${JSON.stringify(c)}`)
    //}
    f.formContainerId = c.formContainer

    f.formContainer = document.getElementById(c.formContainer)

    // Для массива
    //if (!c.itemContainer) {
      //  throw new Error(`itemContainer option is required for list ${JSON.stringify(c)}`)
    //}
    //f.itemContainerId = c.itemContainer

    // для самого поля в родительской форме
    //if (c.container) {
      //  f.containerId = c.container
    //} else {
      //  f.containerId = parentFormContainerId
    //}

/*
    if (c.editmode) {
        f.editmode = c.editmode
        
        if (f.editmode != Form.UNEDITABLE && f.editmode != Form.EDITABLE && f.editmode != Form.READONLY) {
            throw new Error(`Unknown '${c.name}' editmode: ${c.editmode}`)
        }
    } else {
        f.editmode = Form.EDITABLE
    }
    */

    /*
    if (c.showForm) {
        f.showForm = () => {
            if (f.selected) {
                console.log('select')
                this._select(f) // снимаем выделение
            }
            c.showForm()
            f.
        }
    }
*/


    f.schema = this._buildTree(c.schema, f)

    return f
}



Form.prototype._getObjectValue = function(p) {
    let clean, dirty

    switch (p.type) {
    case 'checkboxes':
        clean = [] // массив ID
        dirty = [] // массив объектов

        let key = p.name
        if (p.key) {
            key = p.key
        }

        p.elems.forEach((x, idx) => {
            if (x.checked) {
                let v = p.valueObjects[idx]
                dirty.push(v)

                if (p.scalar) {
                    clean.push(v)
                } else {
                    clean.push(v[key])
                }
            }
        })

        //upd[p.name] = checkedIDs
        break

    case 'radios':
        if (Array.isArray(p.elems)) {
            for (let i=0; i<p.elems.length; i++) {
                if (p.elems[i].checked) {
                    dirty = p.valueObjects[i]
                    if (p.scalar) {
                        clean = dirty
                    } else {
                        clean = dirty[p.name]
                    }                    
                    break
                }
            }
        }

        //upd[p.name] = dirty
        break

    case 'select':
        // отнимаем пустой option
        //console.log('SELECTED INDEX:', p, p.elem.selectedIndex)
        
        // Если элементы еще не загрузились по сети
        if (Array.isArray(p.valueObjects) && p.valueObjects.length > 0) {
            dirty = p.valueObjects[p.elem.selectedIndex - 1]
        }
        if (dirty !== undefined) {
            if (p.scalar) {
                clean = dirty
            } else {
                clean = dirty[p.name]
            }
        }
        break

    case 'checkbox':
        dirty = p.elem.checked
        // fix - объекты вместо скаляра?
        //upd[p.name] = dirty
        clean = dirty
        break

    case '':
        //if (p.contextVar) {
          //  dirty = this._context[p.contextVar]
            //clean = this._context[p.contextVar]
        //} else {
            dirty = p.value
            clean = p.value
        //}
        break

    case 'line':
    case 'text':
        dirty = p.elem.value

        // Обрезаем пробелы
        if (typeof dirty == 'string') {
            dirty = dirty.trim()
        }

        let v;

        // Важно для расчета signature. Ибо до того, как напечатали в поле ввода символ
        // - в signature нет такого property, а когда символ напечатали и удалили, - в
        // signature появится property c пустой строкой или нулем
        if (dirty == '') {
            v = undefined
        } else {
            switch (p.dataType) {
            case 'int':
                v = parseInt(dirty) || 0
                break              
            case 'float':
                v = parseFloat(dirty) || 0
                break

            case 'str':
                v = dirty || ''
                break

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

            default:
                throw new Error(`Unknown type: ${p.dataType}`)
            }
            if (p.get) {
                v = p.get(v)
            }
        }
        
        dirty = v
        clean = v
        break

    //case 'custom-list':
        //return p.custom.getValues()

    case 'model':
        if (p.modelChangeType == 'radio') { 
            dirty = p.model.getSelected(p.modelSelectType)
        } else {
            dirty = p.model.listChecked(p.modelSelectType)
        }

        //console.log('DIRTY model object:', dirty)

        if (dirty !== undefined) {
            if (p.scalar) {
                clean = dirty
            } else {
                clean = dirty[p.name]
            }
        }
        break

    default:
        throw new Error(`bug: unknown property type '${p.type}'`)
    }

    return {
        dirty: dirty,
        clean: clean
    }
}

Form.prototype._disableField = function(p) {
    //console.log('disablefield')
    if (p.type == 'list') {
        p.arr.forEach(item => {
            item.checkbox.disabled = true
        })
        p.btnShow.hidden = true
    } else if (p.type == 'model') {
        p.model.disable()
    } else if (p.elem) {
        p.elem.disabled = true
    } else if (p.elems) {
        p.elems.forEach(elem => {
            elem.disabled = true
        })
    }
}



Form.prototype._createField = function(p, container) {
    //if (p.type == '') {
    //    return
    //}
    //console.log('_createField...', p.name, p.type)
    let control
    let contextValue

    let labelText = p.name
    if (p.label) {
        labelText = p.label
    }
    
    //console.log('_createField: data=', p.data)

    let div = document.createElement('div')
    div.setAttribute('class', 'form-group')

    p.cover = div

    let label = document.createElement('label')
    label.textContent = labelText
    label.classList.add(_labelClass)

    switch (p.type) {
    case '':
        //p.cover = document.createElement('div')

        container.appendChild(p.cover)


        // FIX - генерация события?
        if (p.contextVar) {
            contextValue = this._context[p.contextVar]

            if (contextValue !== undefined) {
                p.value = contextValue

                this.form.selected[p.name] = contextValue
            } else {
                delete this.form.selected[p.name]
            }            
        }

        break

    case 'checkboxes': 
        div.style.display = 'flex'
        div.style.flexDirection = 'column'

        div.appendChild(label)

        p.optionContainer = div
        p.elems = []

        // FIX - генерация события?
        if (p.contextVar) {
            contextValue = this._context[p.contextVar]
            this._fillOptionsField(p, contextValue)
        } else if (p.data) {
            this._fillOptionsField(p, p.data)
        } else if (p.getData) {
            this._fillOptionsField(p, p.getData())
        } else if (p.caching) {
            let cached = p.cache.get()
            if (cached) {
                this._fillOptionsField(p, cached)
            }
        }


        container.appendChild(div)
        break

    case 'radios':
        div.style.display = 'flex'
        div.style.flexDirection = 'column'

        div.appendChild(label)

        p.optionContainer = div        
        p.elems = []

        // FIX - генерация события?
        if (p.contextVar) {
            contextValue = this._context[p.contextVar]
            this._fillOptionsField(p, contextValue)
        } else if (p.data) {
            this._fillOptionsField(p, p.data)
        } else if (p.getData) {
            this._fillOptionsField(p, p.getData())
        } else if (p.caching) {
            let cached = p.cache.get()
            if (cached) {
                this._fillOptionsField(p, cached)
            }
        }

        //console.log('RADIOs CREATED', p.elems)

        

        container.appendChild(div)
        break

    case 'select':
        control = document.createElement('select')
        control.setAttribute('class', 'form-control')

        if (p.width) {
            control.style.width = p.width
        }

        if (p.className) {
            control.classList.add(p.className)
        }

        p.elem = control

        div.appendChild(label)
        div.appendChild(control)

        if (p.comment) {
            // FIX <small class="form-text text-muted">от 15 секунд и выше</small>
            let small = document.createElement('small')
            small.classList.add('form-text', 'text-muted')
            small.textContent = p.comment

            div.appendChild(small)
        }

        // FIX - генерация события?
        if (p.contextVar) {
            contextValue = this._context[p.contextVar]
            this._fillOptionsField(p, contextValue)
        } else if (p.data) {
            this._fillOptionsField(p, p.data)
        } else if (p.getData) {
            this._fillOptionsField(p, p.getData())
        } else if (p.caching) {
            let cached;
            //console.log(p.cache, p.loadFuncContextParam)
            if (p.getLoadFuncArg) {
                let arg = p.getLoadFuncArg(p.form.selected)
                cached = p.cache.get(arg)
            } else {
                cached = p.cache.get()
            }
            //console.log(cached)
            if (cached) {
                this._fillOptionsField(p, cached)
            }
        }

        p.elem.addEventListener('change', () => {            
            this._onElemEvent(p)
        })

        container.appendChild(div)
        break

    case 'line':
        control = document.createElement('input')
        control.setAttribute('type', 'text')
        control.setAttribute('class', 'form-control')

        if (p.width) {
            control.style.width = p.width
        }

        if (p.className) {
            control.classList.add(p.className)
        }

        if (p.placeholder) {
            control.setAttribute('placeholder', p.placeholder)
        }

        control.addEventListener('input', () => {            
            this._onElemEvent(p)
        })
        
        div.appendChild(label)
        div.appendChild(control)

        if (p.comment) {
            // FIX <small class="form-text text-muted">от 15 секунд и выше</small>
            let small = document.createElement('small')
            small.classList.add('form-text', 'text-muted')
            small.textContent = p.comment

            div.appendChild(small)
        }

        container.appendChild(div)

        p.elem = control
        //p.defaultValue = control.value

        // FIX - генерация события?
        if (p.contextVar) {
            contextValue = this._context[p.contextVar]
            p.elem.value = contextValue
        }

        break

    case 'text':
        control = document.createElement('textarea')
        control.setAttribute('class', 'form-control')

        if (p.rows) {
            control.rows = p.rows
        }

        if (p.cols) {
            control.cols = p.cols
        }

        if (p.width) {
            control.style.width = p.width
        }

        if (p.className) {
            control.classList.add(p.className)
        }

        control.addEventListener('input', () => {            
            this._onElemEvent(p)
        })
        
        div.appendChild(label)
        div.appendChild(control)

        if (p.comment) {
            // FIX <small class="form-text text-muted">от 15 секунд и выше</small>
            let small = document.createElement('small')
            small.classList.add('form-text', 'text-muted')
            small.textContent = p.comment
            
            div.appendChild(small)
        }

        container.appendChild(div)

        p.elem = control
        //p.defaultValue = control.value

        // FIX - генерация события?
        if (p.contextVar) {
            contextValue = this._context[p.contextVar]
            p.elem.value = contextValue
        }
        break        

    case 'checkbox':
        control = document.createElement('input')
        control.setAttribute('type', 'checkbox')


        //console.log('CHECKBOX:', control, labelText)

        //label.classList.remove(_labelClass)
        label.innerHTML = ''
        label.appendChild(control)
        label.appendChild(document.createTextNode(` ${labelText}`))

        control.addEventListener('change', () => {            
            this._onElemEvent(p)
        })

        div.appendChild(label)

        container.appendChild(div)

        p.elem = control
        p.labelElem = label

        // FIX - генерация события?
        if (p.contextVar) {
            contextValue = this._context[p.contextVar]
            p.elem.value = contextValue
        }
        break

    //case 'custom-list':
      //  p.custom.create()
        //break

    case 'model':
        //label.style.borderBottom = "1px solid #ddd"
        //label.style.paddingBottom = "5px"
        //p.cover.appendChild(label)

        p.modelOptions.forEach(type => {
            type.container = p.cover
        })
        p.model = new Model(p.modelOptions)


        // FIX - генерация события?
        if (p.contextVar) {
            contextValue = this._context[p.contextVar]
            this._fillOptionsField(p, contextValue)
        } else if (p.data) {
            this._fillOptionsField(p, p.data)
        } else if (p.getData) {
            this._fillOptionsField(p, p.getData())
        } else if (p.caching) {
            let cached;
            //console.log(p.cache, p.loadFuncContextParam)
            if (p.getLoadFuncArg) {
                let arg = p.getLoadFuncArg(p.form.selected)
                cached = p.cache.get(arg)
            } else {
                cached = p.cache.get()
            }
            //console.log(cached)
            if (cached) {
                this._fillOptionsField(p, cached)
            }
        }


        //if (p.changeType == 'few') {
            p.model.onChange = () => {
                this._onElemEvent(p)
            }
        //}
        //p.model

        container.appendChild(p.cover)
        break
    }

    // Отключаем readonly поля при генерации формы (генерация главной формы или 
    // клик по ссылке "добавить")
    if (p.editmode == Form.READONLY && !p.loadFunc) {
        this._disableField(p) 
    }

    p.form.fields[p.name] = p
}

// fix - if initedor not inited
Form.prototype._removeField = function(p) {
    //console.log('_removeField:', p.name)
    let upd = {}

    upd = Object.assign(upd, this._removeDependentFields(p))
    
    if (p.type == 'list') {
        this._removeForm(p)

        // Удаляем элементы связанные со списком items
        p.btnShow = undefined
        p.btnDiv = undefined
        p.btnRemove = undefined
        p.container = undefined
        p.arr = []

        p.cover.remove()
    } else {
        if (p.optionContainer) {
            p.optionContainer = undefined
        }
        // checkbox
        if (p.labelElem) {
            p.labelElem = undefined
        }
       
            //p.custom.reset()
        if (p.model) {
            p.model = undefined
        }
        
        p.elem = undefined

        if (Array.isArray(p.elems)) {
            p.elems = []
        }
        
        if (p.cover) {
            p.cover.remove()
        }

        if (p.selected) {
            p.selected = undefined
        }

        // Срос загружающихся в данный момент
        if (p.loading) {
            p.loading = false
            p.paramSignature = undefined
            p.form.loadingCount--
            this._loadingCount--
        }
    }

    delete p.form.fields[p.name]

    upd[p.name] = undefined

    return upd
}


Form.prototype._buildFork = function(t, form) {
    if (!t) {
        return
    }
    let roads = new Map()

    //console.log('build FORK:', t)

    t.roads.forEach(b => {
        let road = {
            //templateId: b.templateId,
            //templates: b.templates,
            value: b.value,
            values: b.values,
            init: b.init,
            cleanup: b.cleanup,
            //schema: b.schema,
        }

        road.schema = this._buildTree(b.schema, form)
        //branch = Object.assign(branch, tmp)

        if (b.value !== undefined) {
            roads.set(b.value, road)
        } else if (Array.isArray(b.values)) {
            b.values.forEach(value => {
                roads.set(value, road)
            })
        } else {
            throw new Error(`'value' or 'values' option required in road settings`)
        }
    })
    
    return {
        getValue: t.getValue,
        roads: roads,
        //container: t.container,            
    }
}


/*
Form.prototype.getObj = function(schema) {
    let x = this._getObj(schema)
    //console.log('getObj:', x)
    return x
}
*/

Form.prototype._createMainForm = function() {
    //console.log('create main form')
    let schema = this._schema
    let formContainer = this._formContainer

    formContainer.innerHTML = ''

    let divFields = document.createElement('div')
    divFields.setAttribute('class', 'form-group')

    let divButtons = document.createElement('div')
    divButtons.style.marginTop = '1.5rem'
    //divButtons.setAttribute('class', 'buttons-group')
    //divButtons.style.display = 'flex'
    //divButtons.style.justifyContent = 'space-between'

    let btnSubmit = document.createElement('button')
    btnSubmit.setAttribute('class', 'btn btn-outline-success')
    btnSubmit.textContent = this._btnSubmitText
    //btnSubmit.disabled = true fix

    this._btnSubmit = btnSubmit
    this._btnSubmit.addEventListener('click', () => {
        if (this._error) {
            this._error.remove()
        }
        //lett dirty = this.getObj(this._schema)

        //let cleanData = this.cleanObj(dirty)
        //console.log('CLEAN FORM DATA:', this.form.selected)


        if (this._validate) {
            let err = this._validate(this.form.selected)
            if (err) {
                this.showError(err)
            }
        }

        if (this._onSubmit) {
            //this._onSubmit(this._clean)
            this._onSubmit(JSON.parse(JSON.stringify(this.form.selected)))
        }

        /*
        if (this._api) {
            let func, successFunc

            switch (this._mode) {
                case filling:
                    func = this._submitFunc
                    successFunc = this._onSubmitSuccess
                    //console.log('filling: func:', func)
                    break

                case editing:                    
                    func = this._updateFunc
                    successFunc = this._onUpdateSuccess
                    //console.log('editing: func:', func)
                    break
            }
  
            if (func) {
                let snapshotStr = JSON.stringify({
                    version: this._version,
                    clean: this.form.selected,
                    signature: this._signature,
                    mode: this._mode,
                })

                this._api.req({
                    func: func,
                    //data: this._clean,
                    data: this.form.selected,
                    onError: err => {
                        this.restore(snapshotStr)
                        this.showError(err)
                    },
                    onSuccess: resp => {
                        //this.reset()
                        if (successFunc) {
                            successFunc(resp)
                        }                    
                    }
                })
            }
        }
        */
    })

    divButtons.appendChild(btnSubmit)

    let btnRemove = document.createElement('button')
    // ml-5 отступ
    btnRemove.setAttribute('class', 'btn btn-outline-danger ml-5')
    btnRemove.textContent = this._btnRemoveText
    btnRemove.hidden = true

    this._btnRemove = btnRemove

    btnRemove.addEventListener('click', () => {
        if (this._onRemove) {
            this._onRemove()
        }
    })

    divButtons.appendChild(btnRemove)

    this._createSchemaObjects(schema, {append: divFields})

    formContainer.appendChild(divFields)
    formContainer.appendChild(divButtons)



    //console.log('MAIN FORM CREATED!')

    this._signature = JSON.stringify(this.form.selected)
    //this._initSignature =  this._signature
    this._initSnapshot = JSON.stringify({
        version: this._version,
        clean: this.form.selected,
        signature: this._signature,
        mode: this._mode,
    })

    if (!this._enableSubmit) {
        this._btnSubmit.disabled = true
    }

    //console.log("INIT SIGNATURE:", this._signature)
}



Form.prototype._onFormDataChanged = function() {
    this._version++
    // рекурсивно обновляем родительскую коллекцию
    // Считаем подпись
    //let y = this.getObj(this._schema)
    let newSignature = JSON.stringify(this.form.selected)
    /////console.log('SIGNATURE:', newSignature)

    if (this._signature != newSignature) {
        this._btnSubmit.disabled = false
    } else {
        if (!this._enableSubmit) {
            this._btnSubmit.disabled = true
        }
    }
    //let snapshot = 
/*
    localStorage.setItem('x', JSON.stringify({
        signature: this._signature,
        obj: y,
        btnSubmit: this._btnSubmit.disabled,
        mode: this._mode
    }))
    */
}



/*
<div class="form-group">
    <label>ain devices</label>
    <div id="ainDevices"></div>

    // remove + add
    <div class="form-group">
        <button id="add-ain-device" class="btn btn-link">добавить</button>
    </div>
</div>
*/


Form.prototype._createForm = function(schema, formContainer) {
    //console.log('CREATE:', formContainer)
    formContainer.innerHTML = ''

    /*
    let divFields = document.createElement('div')
    divFields.setAttribute('class', 'form-group')

    let divButtons = document.createElement('div')
    divButtons.setAttribute('class', 'buttons-group')

    let btnSubmit = document.createElement('button')
    btnSubmit.setAttribute('class', 'btn')
    btnSubmit.textContent = btnAddText

    let btnClose = document.createElement('button')
    btnClose.setAttribute('class', 'btn btn-link')
    btnClose.textContent = 'close'

    divButtons.appendChild(btnSubmit)
    divButtons.appendChild(btnClose)
    */

    this._createSchemaObjects(schema, {append: formContainer})

    //formContainer.appendChild(divFields)
    //formContainer.appendChild(divButtons)

    return {
        fieldsContainer: formContainer,
        //btnSubmit: btnSubmit,
        //btnClose: btnClose
    }
}

//Form.prototype._createSchemaObjects = function(formId, schema, formContainer) {
Form.prototype._createSchemaObjects = function(schema, anchors) {
    //console.log('_createSchemaObjects...')

    let container;
    if (anchors.append) {
        container = anchors.append
    } else {
        container = document.createDocumentFragment()
    }
    //console.log('create objects:', formId) //, schema)
    schema.forEach(p => {
        //let container = document.getElementById(p.containerId)
        //if (!container) {
          //  throw new Error(`container '${p.containerId}' not found`)
        //}

        if (p.type == 'list') {
            this._createList(p, container)
        } else {
            this._createField(p, container)
        }

        
    })
    
    if (anchors.after) {
        anchors.after.parentNode.insertBefore(container, anchors.after.nextSibling)
    }


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

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

// Заполняем поле вариантами на выбор  
Form.prototype._fillOptionsField = function(p, list) {
    switch (p.type) {
    case 'select':
        p.elem.innerHTML = ''
        //console.log('innerHTML')
        // пустой option
        p.elem.appendChild(document.createElement('option'))
        //console.log('append empty option')

        if (Array.isArray(list)) {
            list.forEach(x => {
                let value

                if (p.scalar) {
                    value = x
                } else {
                    value = x[p.name]
                }

                let option = document.createElement('option')
                option.innerHTML = p.getName(x)
                option.value = value

                p.elem.appendChild(option)

                //console.log('append value option')
            })
        }
        break
    
    case 'checkboxes':
        if (Array.isArray(list)) {
            list.forEach(x => {
                let cb = document.createElement('input')
                cb.setAttribute('type', 'checkbox')
        
                let cbLabel = document.createElement('label')
                //cbLabel.style.paddingTop = '5px'
                //cbLabel.style.paddingBottom = '5px'
        
                cbLabel.appendChild(cb)
                cbLabel.appendChild(document.createTextNode(` ${p.getName(x)}`))
        
                p.optionContainer.appendChild(cbLabel)
        
                p.elems.push(cb)
            })
        }

        p.elems.forEach(checkbox => {
            checkbox.addEventListener('change', () => {
                this._onElemEvent(p)
            })
        })

        break

    case 'radios':
        if (Array.isArray(list)) {
            list.forEach(x => {
                let cb = document.createElement('input')
                cb.setAttribute('type', 'radio')
                cb.setAttribute('name', p.name)
        
                //cb.addEventListener('click', () => {
                //  this._onElemEvent(p, x) // fix
                //})
        
                let cbLabel = document.createElement('label')
                //cbLabel.style.paddingTop = '5px'
                //cbLabel.style.paddingBottom = '5px'
        
                cbLabel.appendChild(cb)
                cbLabel.appendChild(document.createTextNode(` ${p.getName(x)}`))
        
                p.optionContainer.appendChild(cbLabel)
        
                p.elems.push(cb)
                //p.selected = undefined
            })
        }

        p.elems.forEach(radio => {
            radio.addEventListener('click', () => {
                //console.log('RADIO CLICKED', p.selected)
                if (p.selected) {
                    if (p.selected == radio) {
                        radio.checked = false
                        p.selected = undefined
                    } else {
                        p.selected.checked = false
                        p.selected = radio
                    }
                } else {
                    radio.checked = true
                    p.selected = radio
                }
                //console.log('RADIOS ON ELEM EVENT')
                this._onElemEvent(p)
            })
        })
        //console.log('RADIO ELEMS:', p.elems)
        break

    //case 'custom-list':
      //  p.custom.setList(list)
// /        break

    case 'model':
        //console.log('FILL OPTION FIELDS', list)
        p.model.add(p.modelType, list)
        //if (Array.isArray(list)) {
          //  list.forEach(x => {
                
            //})
        //}
        break

    default:
        throw new Error(`bug: unknown property type ${p.type}`)
    }

    p.valueObjects = list
}

/*
Form.prototype._fillCheckboxes = function(p, list) {
    
}


Form.prototype._fillRadios = function(p, list) {
    
}

*/




Form.prototype._createList = function(p, fieldsContainer) {
    //console.log('create list')

/*
    let container = document.getElementById(p.containerId)
    if (!container) {
        throw new Error(`container '${p.containerId}' not found`)
    }
    */
    let labelText = p.name
    if (p.label) {
        labelText = p.label
    }

    let div = document.createElement('div')
    div.setAttribute('class', 'form-group')
    //div.style.border = '1px solid #ddd'

    p.cover = div

    let label = document.createElement('label')
    label.setAttribute('class', 'list-header')
    label.textContent = labelText
    label.style.borderBottom = '1px solid #ddd'
    label.style.paddingBottom = '5px'

    label.classList.add(_labelClass)

    let arrContainer = document.createElement('div')
    //arrContainer.setAttribute('class', 'form-group')

    arrContainer.textContent = p.emptyListMsg

    p.container = arrContainer


    let btnWrapper = document.createElement('div')
    btnWrapper.setAttribute('class', 'form-group')

    //let btn = document.createElement('button')
    //btn.textContent = 'добавить...'
    //btn.setAttribute('class', 'btn btn-link')

    let btnDiv = document.createElement('div')
    btnDiv.setAttribute('class', 'show-link')
    btnDiv.style.marginTop = '0.75rem'

    let btn = document.createElement('a')
    btn.textContent = '+ добавить'
    btn.setAttribute('href', '#')
   
    
    let addBtnDiv = document.createElement('div')
    //addBtnDiv.style.marginTop = '0.5rem'
    //addBtnDiv.style.marginBottom = '0.5rem'

    addBtnDiv.appendChild(btn)

    //console.log('btn', btn)

    btnDiv.appendChild(addBtnDiv)

    p.btnShow = btn
    p.btnDiv = btnDiv // для вставки сообщений об ошибке сразу после btnDiv, но внутри form-group


    p.btnRemove = document.createElement('button')
    p.btnRemove.textContent = 'удалить'
    p.btnRemove.style.color = 'white'
    p.btnRemove.style.backgroundColor = '#ff6961'
    p.btnRemove.setAttribute('class', 'btn')
    p.btnRemove.hidden = true

    p.btnRemove.addEventListener('click', () => {
        this._removeCheckedObjects(p)
        p.btnRemove.hidden = true
        //p.btnShow.hidden = false
    })

    //btnWrapper.appendChild(btn)
    btnWrapper.appendChild(btnDiv)
    btnWrapper.appendChild(p.btnRemove)
    
    div.appendChild(label)
    //div.appendChild(document.createElement('hr'))
    div.appendChild(arrContainer)
    div.appendChild(btnWrapper)

    //container.appendChild(div)

    fieldsContainer.appendChild(div)

    //p.btnSubmit = btn
    //p.container = arrContainer

    p.arr = []

    btn.addEventListener('click', (e) => {
        e.preventDefault()

        if (p.selected) {
            this._select(p, p.selected)
        }

        // Одновременный select
        this._createListItem(p)
        

//        console.log('NEEEEEEEEEEEW ITEEEEEEEEM:', item)


        //this._select(p, p.arr[p.arr.length - 1])
        //let item = p.arr[p.arr.length - 1]
        //item.select.classList.add(p.activeClass)
        //p.selected = item
        //console.log('PUSHED:', item)
        //this._openForm(p)
    })

    //if (this._inEditMode) {
    if (this._mode == editing) {
        if (p.isEditable) {
            btn.hidden = false
        } else {
            btn.hidden = true
        }
    }

    p.form.fields[p.name] = p
}

Form.prototype._createNestedForm = function(t, obj) {
    let formContainer;
            
    if (t.formContainerId) {
        formContainer = document.getElementById(t.formContainerId)
        if (!formContainer) {
            throw new Error(`form container '${t.formContainerId}' not found`)
        }
        formContainer.hidden = false
    } else {
        formContainer = document.createElement('div')
        formContainer.setAttribute('class', t.formContainerClass)

        //formContainer.style.borderLeft = '4px solid #72bcd4'
        //formContainer.style.border = '1px solid #eee'
        formContainer.style.padding = '1rem'
        formContainer.style.marginLeft = '-1rem'
        formContainer.style.marginRight = '-1rem'
        formContainer.style.backgroundColor = '#e8f4f8'
        //formContainer.hidden = true
        t.cover.appendChild(formContainer)
    }
    // fix add showForm()

    t.formContainer = formContainer


    t.dirty = {}

    // fix - return ?
    let r = this._createForm(t.schema, formContainer)

    t.isFormOpened = true

    t.schema.forEach(p => {
        this._setFieldValue(p)
    })
}

Form.prototype._openForm = function(t) {
    //console.log('LOADER clean:', JSON.stringify(t.selected.clean))
    let loader = new Loader({
        api: this._api,
        t: t,
        clean: t.selected.clean,
        context: this._context,
        onError: err => {
            //console.log('Loader.err: ', err)
        },
        onLoaded:() => {
            this._createNestedForm(t)            
        }
    })

    loader.load()

    //console.log('OOOOOOOOOOOOOOOOPEN FORM:', p.isFormOpened)
    /*
    if (t.isFormOpened) {
        this._reset(t)
    } else {
        //console.log('show form')

        let formContainer;
            
        if (t.formContainerId) {
            formContainer = document.getElementById(t.formContainerId)
            if (!formContainer) {
                throw new Error(`form container '${t.formContainerId}' not found`)
            }
            formContainer.hidden = false
        } else {
            formContainer = document.createElement('div')
            formContainer.setAttribute('class', t.formContainerClass)
            //formContainer.hidden = true
            t.cover.appendChild(formContainer)
        }
        // fix add showForm()

        t.formContainer = formContainer

        // fix - return ?
        let r = this._createForm(t.schema, formContainer)


        this._loadDataForCustomElems(t.schema)
*/
        ////////////////////////



        ////////////////////////

        //if (p.formContainer) {
        //    p.formContainer.hidden = false
        //} else if (p.showForm) {
        //    p.showForm()
            //p.isFormOpened = true
        //} else {
            // create container

        //}
        //btn.textContent = 'скрыть'
        //p.btnShow.hidden = true
/*

        t.isFormOpened = true
    }

    // Всегда инициатор, ибо если форма уже была открыта и грузилась - _reset все сбросил
    //t.upd = {}
    
    t.schema.forEach(p => {
        this._setFieldValue(p)
    })

    */
    //if (t.loadingCount == 0) {
    //    this._onFormLoaded(t)
    //}

    //console.log('EEEEEEND OF OOOOOOOOOOOOOOOOPEN FORM:', p.isFormOpened)
}

/*
Form.prototype._closeForm = function(t) {
    this._removeForm(t)    
    t.arr = []
}
*/

// FIX разделить на топ и вложенные, топ снимаем выделение со списка, вложенный обнуляем arr
Form.prototype._removeForm = function(t) {
    //console.log('_removeForm')
    //console.log('CLOOOOOOOOOOOOOOOOSE FORM:', p)
    if (!t.isFormOpened) {
        return
    }

    t.schema.forEach(p => {
        this._removeField(p)
    })
    // скрываем если форма открыта    
    if (t.formContainerId) {
        // внешний div, в документе
        t.formContainer.hidden = true
        t.formContainer.innerHTML = ''        
    } else if (t.hideForm) {
        t.hideForm()        
    } else {
        // inline форма - просто удаляем
        t.formContainer.remove()
    }

    t.fields = {}
    t.settingObj = undefined
    t.upd = undefined
    t.selected = undefined
    t.dirty = {}

    if (t.loadingCount > 0) {
        throw new Error(`Bug: form.loadingCount=${t.loadingCount} after remove fields`)
    }


    t.isFormOpened = false
}

Form.prototype._select = function(t, item) {
    let selected = t.selected
    if (t.selected) {
        t.selected.select.classList.remove(t.activeClass)            
    }
    this._removeForm(t)

    if (selected != item) {
        item.select.classList.add(t.activeClass)
        t.selected = item

        this._openForm(t)
    }
}

Form.prototype._check = function(p) {
    if (p.btnRemove) {
        let q = 0
        p.arr.forEach(item => {
            if (item.checkbox && item.checkbox.checked) {
                q++
            }
        })

        //console.log('checked:', q)

        if (q > 0) {
            //p.btnShow.hidden = true
            p.btnRemove.hidden = false
            p.btnShow.hidden = true
        } else {
            p.btnRemove.hidden = true
            p.btnShow.hidden = false
        }
    }
}

Form.prototype._buildItem = function(p, clean) {
    //console.log('buildItem:', p.name, JSON.stringify(clean))

    let item = {
        clean: clean || {},
    }

    if (clean === undefined) {
        // Создаем пустой item
        item.clean.itemName = p.getItemName()
    }

    let row = document.createElement('div')
    row.style.paddingTop = '3px'
    row.style.paddingBottom = '3px'

    let checkbox = document.createElement('input')
    checkbox.setAttribute('type', 'checkbox')

    let select = document.createElement('div')
    select.style.display = 'inline'
    select.style.padding = '7px'
    select.style.marginLeft = '5px'
    //select.textContent = p.getItemName(obj)
    // dirty - object or undefined

    //let itemName = p.getItemName(p.dirty)
    //item.clean.itemName = itemName
    //select.innerHTML = p.getItemName(dirty)

    select.innerHTML = item.clean.itemName

    row.appendChild(checkbox)
    row.appendChild(select)

    item.elem = row
    item.select = select
    item.checkbox = checkbox

    // Область для select кликов
    select.style.cursor = 'pointer'
    select.addEventListener('click', () => {
        //if (this._loadingCount == 0) {
            //console.log('SELECT ITEM:', JSON.stringify(item.clean))
        this._select(p, item)
        //}        
    })
   
    // Чекбоксы
    checkbox.setAttribute('type', 'checkbox')
    checkbox.addEventListener('change', () => this._check(p))
    
    return item
}



Form.prototype._insertItem = function(p, item) {    
    // Удаляем надпись emptyListMsg
    if (p.arr.length == 0) {
        p.container.innerHTML = ''
    }

    // вставляем в массив
    p.arr.push(item)

    if (!p.isEditable) {
        item.checkbox.disabled = true
    }
    // вставляем в DOM  
    p.container.appendChild(item.elem)
}


Form.prototype._up = function(p) {
    let cleanList = [];

    // цикл по items
    p.arr.forEach(x => {
        cleanList.push(x.clean)
    })
    
    let t = p.form

    if (t.type == 'list') {
        let item = t.selected
        if (item === undefined) {
            return // fix bug
        }

        item.clean[p.name] = cleanList

        this._up(t)
    } else {        
        this.form.selected[p.name] = cleanList

        // calc signature
        this._onFormDataChanged()
    }
}

// Для использования в setFieldValue
// Метод создает item, но не запускает уведомление родителя (ибо уже там есть) и 
// не выбирает (select) добавленный элемент
Form.prototype._appendListItem = function(p, clean) {
    //console.log('pushObject into:', p.name, JSON.stringify(clean))

    let item = this._buildItem(p, clean)
    // FIX обновляем имя в списке объектов
    //item.select.textContent = p.getItemName(obj)
    //item.select.innerHTML = p.getItemName(obj)

    // добавляем новый
    this._insertItem(p, item)
}


Form.prototype._createListItem = function(p) {
    //console.log('pushObject into:', p.name)

    // вместо clean and dirty - undefined
    let item = this._buildItem(p)
    // FIX обновляем имя в списке объектов
    //item.select.textContent = p.getItemName(obj)
    //item.select.innerHTML = p.getItemName(obj)

    // добавляем новый
    this._insertItem(p, item)

    this._select(p, item)

    this._up(p)
}

// recursiveUpdateField
// clean и dirty - объекты вида {name: value}
Form.prototype._updateListItem = function(p, upd) {
    if (p.type == 'list') {
        //console.log(p)

        let item = p.selected

        // Обновляем рекурсивно до топ уровня
        if (item === undefined) {
            throw new Error(`Bug: updateListItem of non selected item`)
        }

        for (let k in upd) {
            let v = upd[k]
            if (v === undefined) {
                //console.log('DELETE PROP:', k, ' IN:', obj)
                delete item.clean[k]
            } else {
                item.clean[k] = v
            }
        }

        let itemName = p.getItemName(p.dirty)

        item.clean.itemName = itemName

        //item.select.textContent = p.getItemName(item.obj)
        item.select.innerHTML = itemName

        this._up(p)
    } else {
        for (let k in upd) {
            let v = upd[k]
            if (v === undefined) {
                delete this.form.selected[k]
            } else {
                this.form.selected[k] = v
            }
        }

        this._onFormDataChanged()
    }
}


Form.prototype._removeCheckedObjects = function(p) {
    if (Array.isArray(p.arr)) {
        let filtered = []
    
        p.arr.forEach(item => {
            if (item.checkbox && item.checkbox.checked) {
                // Снимаем выделение
                if (p.selected == item) {
                    this._select(p, p.selected)
                }

                p.container.removeChild(item.elem)
            } else {
                filtered.push(item)
            }
        })

        p.arr = filtered

        if (p.arr.length == 0) {
            p.container.textContent = p.emptyListMsg
        }

        p.btnShow.hidden = false


        this._up(p)
        /*
        if (p.form.type == 'list') {
            //this._pushObject(p.form, this.getObj(p.form.schema))
            let upd = {}, objectList = [];

            p.arr.forEach(item => {
                objectList.push(item.obj)
            })
            upd[p.name] = objectList
            
            this._updateObject(p.form, upd)
        } else {
            //this._onFormLoaded(this.form)
            this._onFormDataChanged()
        }
        */
    }
}


Form.prototype._removeDependentFields = function(p) {
    //console.log('_removeDependentFields')

    let upd = {}
    if (Array.isArray(p.chains)) {
        // d - depends
        p.chains.forEach(d => {
            upd = Object.assign(upd, this._removeField(d))
        })
    }

    let fork = p.fork

    if (fork) {
        //upd = Object.assign(upd, this._resetFork(p.fork))        

            //console.log('RESET ROAD:', schema)
        let cur = fork.current
        if (!cur) {
            return
        }

        cur.schema.forEach(d => {
            upd = Object.assign(upd, this._removeField(d))
        })

        if (cur.cleanup) {
            cur.cleanup()
        }

        fork.current = undefined        
    }
    return upd
}






// c - custom property
Form.prototype._onElemEvent = function(c) {
    let form = c.form;

    if (form.reseting) {
        return
    }

    // Достаем объект и затем ID
    let tmp = this._getObjectValue(c)

    let cleanValue = tmp.clean
    let dirtyValue = tmp.dirty


    form.dirty[c.name] = dirtyValue

    // ?

    //console.log('ON ELEM EVENT:', c.name)
    //console.log('cleanValue:', cleanValue)
    //console.log('dirtyValue:', dirtyValue)

    // bubble update
    let upd = {}
    upd[c.name] = cleanValue
    this._updateListItem(form, upd)

    // Очищаем зависимые цепочки и развилки, а сам объект события не трогаем
    // FIX обновляем цепочки внутри метода или как сейчас?
    //let upd = Object.assign(upd, )
    this._removeDependentFields(c)

    //console.log('SELECTED OBJECT:', form.selected)
    
    if (dirtyValue !== undefined) {
        // Общий контейнер для всех новых полей
        let container = document.createDocumentFragment()
        
        c.chains.forEach(d => {
            // создаем DOM элементы
            this._createField(d, container)

            if (d.loadFunc) {
                this._loadData(d)
            } else if (d.data) {
                this._fillOptionsField(d, d.data)
                this._setFieldValue(d)
            } else {
                this._setFieldValue(d) // FIX ????
            }
        })

        let f = c.fork
        if (f) {
            let cleanId = cleanValue
            if (f.getValue) {
                // ВАЖНО!
                // getValue на вход получает объект целиком
                cleanId = f.getValue(dirtyValue)
            }

            //console.log('fork clean param:', cleanId)

            // затем устанавливаем новую (если есть)
            let road = f.roads.get(cleanId)
            if (road) {
                //console.log('SET CURRENT ROAD:', JSON.stringify(road))

                if (road.init) {
                    road.init()
                }
                //console.log('CONTAINER:', c.cover)
                this._createSchemaObjects(road.schema, {after: c.cover})

                // в цикле для  custom элементов загружаем loadFunc
                road.schema.forEach(x => {
                    if (x.loadFunc) {
                        this._loadData(x)
                    } else {
                        this._setFieldValue(x)
                    }
                })

                f.current = road

                //road.schema.forEach(x => {
                    //this._setFieldValue(x)
                //})
            }
        }        
        // Вставляем все новые поля после поля события
        c.cover.parentNode.insertBefore(container, c.cover.nextSibling)
    }

    if (c.onChange) {
        c.onChange(dirtyValue)
    }
}


// Загружает данные для построения формы (openForm, init, _select)
// Опциональный объект с уже заполненными полями - clean
function Loader(opt) {
    if (!opt.t.selected) {
        throw new Error(`Where is selected?`)
    }
    this._counter = 0
    this._api = opt.api
    // Для отслеживания - переключил юзер item или нет пока загружались данные
    // и для доступа к схеме
    this._t = opt.t
    // Для отслеживания - переключил юзер item или нет пока загружались данные
    this._selected = this._t.selected
    this._clean = opt.clean || {}
    this._context = opt.context
    
    this._onError = opt.onError
    this._onLoaded = opt.onLoaded
}

// Метод загружает данные и добавляем в кэш элементов. Не занимается отображением данных
Loader.prototype._loadElem = function(p, id) {
    // Если значение уже закешировано - запрос не отправляем
    if (p.caching) {
        let v = p.cache.get(id)
        if (v) {
            return
        }
    }

    this._counter++

    //let arg;
    if (p.getLoadFuncArg) {
        // параметр функции - это объект-снимок формы со всеми заполненными полями 
        // на текущий момент. Форма та, которой принадлежит свойство p.
        id = p.getLoadFuncArg(this._selected) // FIX whole form object
    } 
    
    console.log(`_loadElem: ${p.name} = ${JSON.stringify(id)}`)

    this._api.req({
        func: p.loadFunc,
        data: id,
        onError: err => {
            //console.log(err)
            // Прерываем загрузку
            this._onError(err)
        },
        onSuccess: resp => {
            //console.log(resp)
            if (p.responseModifier) {
                resp = p.responseModifier(resp)
            }
            // Добавляем в кэш
            if (p.loadFuncContextParam) {
                p.cache.set(undefined, resp)
            } else {
                p.cache.set(id, resp)
            }

            // resp - это список объектов (например список checboxes или options в select)
            // this._clean - это заполненные поля (ID-шники)
            // нужна функция, которая из списка объектов методом найдет нужный по ID
            // (методом перебора).


            let cleanValue = this._clean[p.name]
            let dirtyValue = this._findSelectedObject(p, resp, cleanValue)

            if (dirtyValue !== undefined) {
                if (Array.isArray(p.chains)) {
                    p.chains.forEach(d => {
                        //
                        if (d.loadFunc) {
                            this._loadElem(d)
                        }
                    })
                }

                let fork = p.fork
                if (fork) {
                    // 
                    let cleanId = cleanValue
                    if (fork.getValue) {
                        // ВАЖНО!
                        // getValue на вход получает объект целиком
                        cleanId = fork.getValue(dirtyValue)
                    }

                    //console.log('fork clean param:', cleanId)

                    // затем устанавливаем новую (если есть)
                    let road = fork.roads.get(cleanId)
                    if (road) {
                        road.schema.forEach(d => {
                            if (d.loadFunc) {
                                this._loadElem(d)
                            }
                        })
                    }
                }
            }

            this._counter--

            if (this._counter == 0) {
                if (this._selected == this._t.selected) {
                    this._onLoaded()
                }
            }
        }
    }) 
}

Loader.prototype._findSelectedObject = function(p, resp, id) {
    if (!Array.isArray(resp)) {
        return
    }
    if (p.scalar) {
        // это список id, а не список объектов
        if (p.type == 'checkboxes') {
            // fix добавить сюда модель с чекбоксами
            if (!Array.isArray(id)) {
                return []
            }
            let result = []

            // временный словарь для удобной фильтрации
            let checkedIDs = new Map()

            id.forEach(checkedID => {
                checkedIDs.set(checkedID, true)
            })

            let key = p.name
            if (p.key) {
                key = p.key
            }
            for (let i=0; i<resp.length; i++) {
                let v = resp[i]
                if (checkedIDs.has(v)) {
                    result.push(v)
                }
            }

            return result
        } else {
            for (let i=0; i<resp.length; i++) {
                if (resp[i] == id) {
                    return id
                }
            }
        }        
    } else {
        // это список объектов
        if (p.type == 'checkboxes') {
            // fix добавить сюда модель с чекбоксами

            // id - это список ID, а нам нужно найти список объектов            
            if (!Array.isArray(id)) {
                return []
            }
            let result = []

            // временный словарь для удобной фильтрации
            let checkedIDs = new Map()

            id.forEach(checkedID => {
                checkedIDs.set(checkedID, true)
            })

            let key = p.name
            if (p.key) {
                key = p.key
            }
            for (let i=0; i<resp.length; i++) {
                let v = resp[i]
                if (checkedIDs.has(v[key])) {
                    result.push(v)
                }
            }

            return result
        } else {
            for (let i=0; i<resp.length; i++) {
                let v = resp[i]
                if (v[p.name] == id) {
                    return v
                }
            }
        }
    }
}

Loader.prototype.load = function() {
    //console.log('LOADER.loading....')
    this._t.schema.forEach(p => {
        if (p.loadFunc) {
            if (p.loadFuncContextParam) {
                let param = this._context[p.loadFuncContextParam]
                this._loadElem(p, param)
            } else {
                this._loadElem(p)
            }
        }
    })

    if (this._counter == 0) {
        if (this._selected == this._t.selected) {
            this._onLoaded()
        }
    }
}


// po - parent object
Form.prototype._loadData = function(p) {
    
    let arg;
    if (p.getLoadFuncArg) {
        // параметр функции - это объект-снимок формы со всеми заполненными полями 
        // на текущий момент. Форма та, которой принадлежит свойство p.
        arg = p.getLoadFuncArg(p.form.selected, this.form.selected)
    }

    console.log(`_loadData: ${p.name} = ${JSON.stringify(arg)}`)

    let paramSignature = JSON.stringify(arg)

    //onsole.log('_LOAD DATA ____________________', p.name, p.caching)

    if (p.caching) {
        let list = p.cache.get(paramSignature)
        if (list) {
            //console.log('_loadData (from cache):', p, arg)

            // Заполняем поля выбора (select, radios, checkboxes) объектами из списка, 
            // который прислал сервер
            this._fillOptionsField(p, list)

            // Выбираем элемент(ы), если форма в режиме редактирования
            // если режим редактирования этой формы
            this._setFieldValue(p)
            // Закешированные данные нашли, поэтому выходим
            return
        }
        
    }

    if (p.loading) {
        if (p.paramSignature === paramSignature) {
            // Запрос уже отправлен, ничего не делаем
            // Пример такой ситуации: юзер кликнул по первому объекту, на сервер ушел запрос,
            // затем кликнул по второму объекту и снова по первому.
            return
        }
        // Обновляем только сигнатуру параметра, loadingCount уже увеличен, флаг loading уже выставлен
        p.paramSignature = paramSignature
    } else {
        p.loading = true
        p.paramSignature = paramSignature
        
        p.form.loadingCount++
        this._loadingCount++
    }
    
    //console.log('_loadData (loading...):', p, arg)

    this._api.req({
        func: p.loadFunc,
        data: arg,
        onError: err => {  
            if (!p.loading) {
                // UI элемент был удален, поэтому сообщение об ошибке не нужно
                // loadingCount уже сброшен при удалении элемента
                return
            }

            if (p.paramSignature !== paramSignature) {
                // Уже отправлен более свежий запрос, поэтому сообщение об ошибке не нужно
                return
            }

            p.loading = false
            p.paramSignature = undefined

            p.form.loadingCount--
            this._loadingCount-- 

            p.divLoadError = insertError({
                //after: item.elem.getAnchor(), 
                after: p.cover, 
                err: err,
                class: this._errorClass
            })
            
            //console.log(`item ${p.name} loaded. Loading count-- (error): ${this._loadingCount}`)

            //if (p.form.loadingCount == 0) {
            //    this._onFormLoaded(p.form)
            //}
        },
        onSuccess: resp => {
            if (!p.loading) {
                // UI элемент был удален, поэтому загруженные данные больше не нужны
                // loadingCount уже сброшен при удалении элемента
                //console.log(`_loadData: prop ${p.name} not loading`)
                return
            }

            if (p.paramSignature !== paramSignature) {
                // Уже отправлен более свежий запрос, поэтому загруженные данные больше не нужны
                //console.log(`_loadData: prop ${p.name} paramSignature changed (has: '${p.paramSignature}', expect: '${paramSignature}')`)
                return
            }

            p.loading = false
            p.paramSignature = undefined

            p.form.loadingCount--
            this._loadingCount-- 

            if (p.responseModifier) {
                resp = p.responseModifier(resp)
            }
            // Добавляем в кэш
            if (p.caching) {
                p.cache.set(paramSignature, resp)
            }

            //console.log(`item ${p.name} loaded. Loading count--: ${this._loadingCount}`)
            
            // Заполняем поля выбора (select, radios, checkboxes) объектами из списка, 
            // который прислал сервер
            this._fillOptionsField(p, resp)

            // Выбираем элемент(ы), если форма в режиме редактирования
            // если режим редактирования этой формы
            this._setFieldValue(p)
        }
    })
}






// FIX сделать метод локальным для формы?
Form.prototype._onFormLoaded = function(loader) {
    //console.log('ON_FORM_LOADED:', form, ' loadingCount:', this._loadingCount)
/*
    let t = loader.t

    
    if (t.selected != loader.selected) {
        return
    }


    // create form
    this._createSchemaObjects(t.schema)

    loader.onFormLoaded()

    t.schema.forEach(p => {
        this._setFieldValue(p, t.selected.clean, t.selected.dirty)
    })
    */

    //let upd = form.upd

    //form.settingObj = undefined
    //form.upd = undefined

    //this._updateObject(form, upd)

    //this.enable()

    // editing mode остается до reset формы
    // FIXXXXX
    /*
    if (this._mode == restoring) {
        this._mode = this._restoringMode
        this._signature = this._restoringSignature
        this._btnSubmit.disabled = this._restoringBtnSubmit

        this._restoringMode = undefined
        this._restoringMode = undefined
        this._restoringBtnSubmit = undefined
    }
    */

    //if (this._mode == editing) {
    //    if (!this.form.isEditable) {
            //this._btnSubmit.disabled = true
            //this._btnReset.disabled = true
     //   }
    //}

    //if (form.type != 'list') {

    //}
}

// Отключает все элементы формы во время загрузки данных с сервера
/*
Form.prototype.disable = function() {
    this._schema.forEach(p => {
        this._disableField2(p)
    })

    this._btnSubmit.disabled = true
    this._btnReset.disabled = true
}

Form.prototype.enable = function() {
    this._schema.forEach(p => {
        this._enableField2(p)
    })

    this._btnSubmit.disabled = false
    this._btnReset.disabled = false
}
*/

// Важный метод сбрасывает все поля главной формы и скрывает все открытые формы
Form.prototype.reset = function(opt) {
    //console.log('RESET')


    try {
        let snapshot = JSON.parse(this._initSnapshot)
        this._signature = JSON.stringify(snapshot.clean)    
        this.form.selected = snapshot.clean
    } catch (e) {
        //console.log(this)
        throw new Error(`form not initialized:`)
        this.form.selected = {}
    }

    

    this._reset(this.form)

    //this._btnSubmit.disabled = true

    this._mode = filling

    if (this._error) {
        this._error.remove()
    }


    //if (opt && opt.ignoreButtons) {
        if (!this._enableSubmit) {
            this._btnSubmit.disabled = true
        }
        this._btnSubmit.textContent = this._btnSubmitText

        this._btnRemove.hidden = true
    //}

    //this._onFormDataChanged()

    //console.log('reset mode:', this._mode)
    //console.log('reset clean:', this.form.selected)
}

// Метод сбрасывает значения всех элементов формы, удаляет зависимые элементы 
// и сбрасывает списки, а именно - очищает список, закрывает (удаляет) форму. 
// Причем форму закрывает рекурсивно, корректно удаляя все элементы вложенных форм.
// Сброс формы важен чтобы правильно очистить loadingCount, поле loading и т.д.
Form.prototype._reset = function(t) {
    //console.log('_RESET', t)

    // Флаг, который позволит в методе onElemEvent игнорировать события при 
    // сбросе формы
    t.reseting = true

    t.schema.forEach(p => {
        if (p.type == 'list') {
            //console.log('RESET LIST FIELD:', p)
            this._removeForm(p)
            
            // Делаем reset, не удаляя элементы ???

            p.arr = []
            p.container.innerHTML = ''
            p.container.textContent = p.emptyListMsg
            p.btnShow.hidden = false
            p.btnRemove.hidden = true
        } else {
            var contextValue

            if (p.contextVar) {
                contextValue = this._context[p.contextVar]                
            }

            switch (p.type) {
            case '':
                if (p.contextVar) {
                    p.value = contextValue
                    //console.log('"" CONTEXT VALUE:', p.value)


                    if (contextValue !== undefined) {
                        this.form.selected[p.name] = contextValue
                    } else {
                        delete this.form.selected[p.name]
                    }

                } else {
                    p.value = undefined
                }                
                break

            case 'line':
            case 'text':
                if (p.contextVar) {
                    p.elem.value = contextValue

                    if (contextValue !== undefined) {
                        this.form.selected[p.name] = contextValue
                    } else {
                        delete this.form.selected[p.name]
                    }
                } else {
                    p.elem.value = ''
                }

                if (p.editmode != Form.READONLY) {
                    p.elem.disabled = false
                }
                // fix dispachevent
                break

            case 'checkbox':
                if (p.contextVar) {
                    p.elem.value = contextValue
                } else {
                    p.elem.checked = false
                }
                
                if (p.editmode != Form.READONLY) {
                    p.elem.disabled = false
                }
                break

            case 'select':
                if (p.contextVar) {
                    this._fillOptionsField(p, contextValue)
                }

                if (p.valueObjects) {
                    if (p.valueObjects.length > 0) {
                        p.elem.selectedIndex = 0
                    } else {
                        p.elem.selectedIndex = -1
                    }
                    if (p.editmode != Form.READONLY) {
                        p.elem.disabled = false
                    }
                    p.elem.dispatchEvent(new Event('change'))
                }
                break

            case 'radios':
                if (p.contextVar) {
                    this._fillOptionsField(p, contextValue)
                }
                // fix context
                // fix dispachevent - if checked changed
                p.elems.forEach(elem => {
                    // fix - if checked
                    if (elem.checked) {
                        //elem.checked = false
                        //console.log('RADIOS reset: before click is checked', elem.checked)
                        elem.dispatchEvent(new Event('click'))
                        //console.log('RADIOS reset: after click is checked', elem.checked)
                    }

                    if (p.editmode != Form.READONLY) {
                        elem.disabled = false
                    }
                })
                break

            case 'checkboxes':
                if (p.contextVar) {
                    this._fillOptionsField(p, contextValue)
                }
                // fix context
                // fix dispachevent - if checked changed
                p.elems.forEach(elem => {
                    // fix - if checked
                    if (elem.checked) {
                        elem.checked = false
                    }

                    if (p.editmode != Form.READONLY) {
                        elem.disabled = false
                    }
                })
                break

            case 'model':
                // FIX - just enable?
                //p.model.enable()
                p.modelOptions.forEach(type => {
                    type.container = p.cover
                })
                p.model = new Model(p.modelOptions)
        
        
                // FIX - генерация события?
                /*
                if (p.contextVar) {
                    this._fillOptionsField(p, contextValue)
                } else if (p.data) {
                    this._fillOptionsField(p, p.data)
                } else if (p.getData) {
                    this._fillOptionsField(p, p.getData())
                } else {
                    let cached = p.cache.get()
                    if (cached) {
                        this._fillOptionsField(p, cached)
                    }
                }
        */
        
                //if (p.changeType == 'few') {
                    p.model.onChange = () => {
                        this._onElemEvent(p)
                    }
                break

            //case 'custom-list':
                // fix custom reset 
            //    break
            }
        }

        if (p.loading) {
            p.loading = false
            p.paramSignature = undefined
            
            t.loadingCount-- // form
            this._loadingCount--
        }

        this._removeDependentFields(p)
    })

    t.settingObj = undefined

    t.reseting = false
    //localStorage.removeItem('x')
}


Form.prototype.hideError = function() {
}




/*
Form.prototype.restore = function(s) {
    console.log('restore:', s)
    
    //if (!obj) {
    //    return
    //}

    if (this._waitingObj) {
        throw new Error(`Can't restoreObj while waiting`)
    }

    // ВАЖНО! 
    // Обнулять _loadingCount нельзя, ибо в данный момент может грузится форма
    this._mode = restoring
    this.form.settingObj = s.obj
    this._restoringSignature = s.signature
    this._restoringMode = s.mode
    this._restoringBtnSubmit = s.btnSubmit
    
    this._schema.forEach(p => {
        this._setFieldValue(p)
    })

    if (this._loadingCount > 0) {
        //console.log('disable while loading')
        //this.disable() // отключаем форму
        // Отключать ли submit?
    } else {
        console.log('restore, no loading elems')
        // fix btnsubmit
        this._onFormDataChanged()
        this._onFormLoaded() // fix
    }        
}
*/


// restore - восстанавливает mode, obj, signature и рассчитывает состояние кнопки
Form.prototype.restore = function(snapshotStr) {
    let snapshot = JSON.parse(snapshotStr)
    if (this._version == snapshot.version) {
        return
    }

    let obj = snapshot.dirty
    
    //console.log('restore:', obj)    

    //if (!obj) {
    //    return
    //}

    // Создаем копию объекта
    //obj = JSON.parse(JSON.stringify(obj))

    this.reset()

    this.form.selected = obj // ? fix что делать с дефолтами

    //this._cleanDirtyObject(this._schema, obj, this._clean)
    //this._cleanDirtyObject(this._schema, obj, this.form.selected.clean)

    //console.log('CLEANED:', this.form.selected.clean)

    //console.log('restoring')
    
    this._mode = snapshot.mode

    let loader = new Loader({
        api: this._api,
        t: this.form,
        clean: this.form.selected,
        context: this._context,
        onError: err => {
            //console.log('Loader.err: ', err)
        },
        onLoaded:() => {
            this._schema.forEach(p => {
                this._setFieldValue(p)
            })

            let newSignature = JSON.stringify(this.form.selected)

            this._signature = snapshot.signature

            if (this._signature != newSignature) {
                this._btnSubmit.disabled = false
            } else {
                if (!this._enableSubmit) {
                    this._btnSubmit.disabled = true
                }
            }

            //console.log('restored signature:', this._signature)
            
        }
    })

    loader.load()
}

// edit
Form.prototype.edit = function(obj, isRemoveAllowed) {
    
    //console.log('EDIT:', obj)    

    if (!obj) {
        return
    }

    obj = JSON.parse(JSON.stringify(obj))

    //this.hide()
    this.reset({
        ignoreButtons: true,
    })

    this.form.selected = obj // ? fix что делать с дефолтами


    //this.form.selected.dirty = obj
    //this.form.selected.clean = {}

    //this._cleanDirtyObject(this._schema, obj, this._clean)
    //this._cleanDirtyObject(this._schema, obj, this.form.selected.clean)

    //console.log('CLEANED:', this.form.selected.clean)

    //console.log('editing')

    this._mode = editing

    let loader = new Loader({
        api: this._api,
        t: this.form,
        clean: this.form.selected,
        context: this._context,
        onError: err => {
            //console.log('Loader.err: ', err)
            //this.show()
        },
        onLoaded:() => {
            this._schema.forEach(p => {
                this._setFieldValue(p)
            })

            this._signature = JSON.stringify(this.form.selected)
            if (!this._enableSubmit) {
                this._btnSubmit.disabled = true
            }
            this._btnSubmit.textContent = this._btnUpdateText

            if (isRemoveAllowed) {
                this._btnRemove.hidden = false
            }

            //console.log('edit.signature:', this._signature)
            //console.log('edit.mode:',  this._mode)

            //this.show()
        }
    })

    loader.load()
}


Form.prototype.fill = function(obj) {
    
    //console.log('fill:', obj)    

    if (!obj) {
        return
    }

    obj = JSON.parse(JSON.stringify(obj))

    this.reset()

    this.form.selected = obj // ? fix что делать с дефолтами

    //this.form.selected.dirty = obj
    //this.form.selected.clean = {}

    //this._cleanDirtyObject(this._schema, obj, this._clean)
    //this._cleanDirtyObject(this._schema, obj, this.form.selected.clean)

    //console.log('CLEANED:', this.form.selected.clean)

    this._mode = filling

    let loader = new Loader({
        api: this._api,
        t: this.form,
        clean: this.form.selected,
        context: this._context,
        onError: err => {
           /// console.log('Loader.err: ', err)
        },
        onLoaded:() => {
            this._schema.forEach(p => {
                this._setFieldValue(p)
            })

//            this._signature = JSON.stringify(this.form.selected.clean)
  //          this._btnSubmit.disabled = true

    //        console.log('edit:', this._signature)
        }
    })

    loader.load()
}


Form.prototype._setFieldValue = function(f) {
    if (f.loading) {
        //console.log(`prop ${f.name} loading`)
        return
    }

    let obj 
    
    if (this.form == f.form) {
        obj = f.form.selected
    } else {
        obj = f.form.selected.clean
    }

    let v = obj[f.name]

    //console.log(`_setFieldValue: ${f.name}=${v}`)

    if (f.type == 'list') {
        //this._openForm(f)

        if (Array.isArray(v)) {
            // fix
            v.forEach((itemObj, idx) => {
                //let cObj = cleanObj[f.name][idx]
                //this._appendListItem(f, cObj, dObj)
                this._appendListItem(f, itemObj)
            })
        }
        
        if (this._mode == editing) {
            if (!f.isEditable) {
                this._disableField(f)
            }
        }

    } else {
        switch (f.type) {  
        case 'checkboxes':
            // fix - event
            let checkedIDs = {}

            if (Array.isArray(v)) {
                v.forEach(x => {
                    checkedIDs[x] = true
                })
    
                if (f.scalar) {
                    f.valueObjects.forEach((item, idx) => {
                        //console.log('CHECK IF:', item, idx, checkedIDs)
                        if (checkedIDs[item]) {
                            f.elems[idx].checked = true
                        }
                    })
                } else {
                    let key = f.name
                    if (f.key) {
                        key = f.key
                    }
                    f.valueObjects.forEach((item, idx) => {
                        //console.log('CHECK IF:', item, idx, checkedIDs)
                        if (checkedIDs[item[key]]) {
                            f.elems[idx].checked = true
                        }
                    })
                }
            }
            break
    
        case 'radios':
            //console.log('RADIOS:', f.valueObjects)
            if (v && Array.isArray(f.valueObjects)) {
                for (let i=0; i<f.valueObjects.length; i++) {
                    let item = f.valueObjects[i]

                    if (f.scalar) {
                        if (item == v) {
                            let radio = f.elems[i]
                            //radio.checked = true
                            // Поле selected важно для снятия отметки при клике 
                            // на уже выделенном radio
                            //f.selected = radio
                            //console.log
                            radio.dispatchEvent(new Event('click'))
                            break
                        }
                    } else {
                        if (item[f.name] == v) {
                            let radio = f.elems[i]
                            //console.log('RADIOS: before click is checked', radio.checked)
                            radio.dispatchEvent(new Event('click'))
                            break
                        }
                    }
                }
            }
            break
        
        case 'select':
            f.elem.value = v
            f.elem.dispatchEvent(new Event('change'))
            break
    
        case '':
            f.value = v
            this._onElemEvent(f)
            break

        case 'line':
        case 'text':
            if (v !== undefined) {                
                if (f.dataType == 'hex') {
                    if (v) {
                        v = base64ToHex(v)
                    }
                } else if (f.dataType == 'int' && v === 0) {
                    v = ''
                }

                if (f.set) {
                    f.elem.value = f.set(v)
                } else {
                    f.elem.value = v
                }
            }
            //f.elem.dispatchEvent(new Event('input'))
            this._onElemEvent(f)
            break

        case 'checkbox':
            f.elem.checked = v
            f.elem.dispatchEvent(new Event('change'))
            break

        case 'model':
            //console.log('modelSelectType:', f.modelSelectType)
            //console.log('modelChangeType:', f.modelChangeType)
            

            if (f.modelChangeType == 'radio') {
                f.model.setSelected(f.modelSelectType, v)
            } else {
                f.model.checkList(f.modelSelectType, v)
            }
            this._onElemEvent(f)
            break
        }

        // Отключать поля можно только если форма в режиме редактирования (именно топ форма)
        if (this._mode == editing) {
            if (f.editmode == Form.UNEDITABLE) {
                this._disableField(f)
            }
        }
    }
}




function insertError(opt) {
    let div = document.createElement('div')
    if (opt.class) {
        div.className = opt.class
    }
    let msg;
    if (typeof opt.err == 'string') {
        msg = opt.err
    } else {
        msg = opt.err.message
    }
    div.textContent = msg

    if (opt.after) {
        opt.after.parentNode.insertBefore(div, opt.after.nextSibling)
    } else if (opt.before) {
        // например перед submit кнопкой
        opt.before.parentNode.insertBefore(div, opt.before)
    }
    return div
}

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
}


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;
}

// fix - hide открытые вложенные формы
Form.prototype.hide = function() {
    this._formContainer.hidden = true
}

// fix - show открытые вложенные формы
Form.prototype.show = function() {
    this._formContainer.hidden = false
}