

function copyTemplateInto(templateId, dstId) {
    let t = document.getElementById(templateId)
    if (!t) {
        throw new Error(`tag id=${templateId} not found`)
    }
    //let dst = document.getElementById(dstId)
    //if (!dst) {
    //    throw new Error(`tag id=${dstId} not found`)
    //}
    let dst = getElemSafe('container', dstId)
    let node = t.content.cloneNode(true)
    dst.innerHTML = ''
    dst.appendChild(node)
}

function Router() {    
    let u = new URL(location.href)
    // Базовый url вида http://localhost:8081
    this._origin = u.origin
    this._routes = {}
}


// goto . Аргумент - полный URL, либо path + searchParams вида '/configs?deviceID=7'.
// Полный URL обычно получаем при чтении текущего адрес location.href, либо при чтении
// ссылки somelink.href. Path получим если меню реализовать в виде ListModel, где
// ключем очевидно будет path, а не полный URL.
Router.prototype.goto = function(url, params) {
    if (!url) {
        throw new Error(`Wrong 'url' argument: ${url}`)
    }

    console.log('goto:', url)

    if (url.startsWith('/')) {
        url = this._origin + url
    }

    let u = new URL(url)
    let r = this._routes[u.pathname]
    if (!r) {
        return
    }

    let path = u.pathname    

    if (params) {
        //let searchParams = new URLSearchParams()
        let searchParams = u.searchParams
        for (let name in params) {
            searchParams.set(name, params[name])
        }
        let queryString = searchParams.toString()
        if (queryString !== '') {
            path += '?' + queryString
        }
    } else {
        if (u.searchParams.toString()) {
            //console.log('serachParams:', u.searchParams.toString())
            path += '?' + u.searchParams
        }
    }

    path += u.hash
    url = this._origin + path


    console.log('route:', r)
    console.log('group:', this._currentGroup)

    if (r.group != this._currentGroup) {
        if (this._currentGroup) {
            if (r.group.base != this._currentGroup.base) {
                location.href = url
                return
            }
        }
        this._currentGroup = r.group
        if (r.group.init) {
            //console.log('call init and wait')
            r.group.init((ctx) => {
                this._ctx = ctx
                this._processRoute(r, url, path)
            }) 
        } else {
            this._ctx = {}
            this._processRoute(r, url, path)
        }
    } else {
        this._processRoute(r, url, path)
    }
}

Router.prototype._processRoute = function(r, url, path) {
    if (r.group.toggle) {
        r.group.toggle(this._ctx, r.data)
    }

    let state = {}
    state['omg.router.url'] = url
    if (url != location.href) {
        console.log('pushState:', url)
        // В первую очередь проверка позволяет не добавлять в историю страницу,
        // которая только что загрузилась (реакция на событие DOMContentLoaded)
        history.pushState(state, "", path)
    } else {
        console.log('replaceState:', url)
        history.replaceState(state, "", path)
    }

    r.func(this._ctx)
}

Router.prototype.add = function(routeGroup) {
    let group = {
        base: routeGroup.base,
        init: routeGroup.init,
        toggle: routeGroup.toggle
    }
    routeGroup.routes.forEach((route) => {
        if (!route.path) {
            throw new Error(`Empty path in route: ${JSON.stringify(route)}`)
        }
        this._routes[route.path] = {
            func: route.func,
            data: route.data,
            group: group             
        }
    })
}

// Читает path из URL и запускает соответсвующий обработчик
Router.prototype.init = function() {
    document.addEventListener('DOMContentLoaded', () => {
        let links = document.getElementsByClassName('route')
        //console.log(links)
        for (let i=0; i<links.length; i++) {
            let link = links[i]
            //console.log(link)
            link.addEventListener('click', (e) => {
                let u = new URL(link.href)
                let r = this._routes[u.pathname]
                if (r) {
                    // if current path in routes
                    e.preventDefault()
                    //console.log('CLICK:', link.href)
                    this.goto(link.href)
                }  else {
                    console.log("not in routes")
                }             
            })
        }

        window.addEventListener('popstate', (e) => {
            if (!e.state) {
                return
            }
            let url = e.state['omg.router.url']
            //console.log('popstate:', url)
            this.goto(url)
        })

        this.goto(location.href)
    })
}

/**
 * A opt
 * @typedef {Object} Opt
 * @property {string} key - ключ.
 * @property {Element} cover - обложка
 */


/**
 * 
 * @param {Opt} opt - объект 
 */

/*
opt - Опции
- container {Element} HTML-элемент, контейнер для элементов модели
- cover * {Element} HTML-элемент, внешний контейнер для всей модели. Нужен для одного
кейса - когда необходимо показать сообщение об ошибке (например, ошибка загрузки по API),
а добавление ошибки после container невозможна. Например, container - это TBODY.
По умолчанию - нет.
- unique * {func} Функция вычисляет hashcode для каждого элемента модели и не позволяет
добавлять в модель дубликаты. Применяется в методах add() и updateSelected()
- activeClass * {string} CSS класс для выбранного (кликом мыши) элемента модели.
По умолчанию 'selected'.
- errorClass * {string} CSS класс для сообщения об ошибке (например, при загрузке модели 
по API или удалении элементов по API). По умолчанию 'errmsg'.
- messageClass * {string} CSS класс для сообщения о пустой модели.
- emptyMessage * {string} Сообщение, которое нужно показать если setList устанавливает 
пустую модель.
- key * {string} Имя id-поля объекта. Используется в единственном месте - для AJAX-запроса 
на удаление элемента.
- btnRemove * {Element} HTML-кнопка для удаления элементов модели. 
Если объявлена removeFunc - удаление будет происходить AJAX запросами к API, иначе сразу
из модели. Если по умолчанию кнопка disabled - модель сама будет включать и отключать
кнопку в зависимости от проставленных галочек напротив элемментов модели.
- template {Object} Настройки шаблона для рендера элемента модели
    - id {string} ID template элемента 
    - renders * {Object} Рендер-функции шаблонов (смотри Template)
- sort * {Object} Сортировка элементов модели
    - key * {string} имя поля по которому сортируем
    - func * {func} собственная функция сортировки
    - reverse * {boolean} флаг, показывает что все нужно сортировать наоборот
По умолчанию не сортирует, а элементы добавляет в конец списка. Если указать reverse=true,
добавлять элементы будет в начало списка. Сортировка используется в методах add() и 
updateSelected()
- ajax * {Object} Настройки для загрузки модели и/или удаления элементов модели по API
    - api {API} смотри API
    - listFunc * {string} имя API метода для загрузки модели
    - removeFunc * {string} имя API метода для удаления элемента модели по ID. Имя поля
    объекта, который хранит ID задается в опции key.


*/
function ListModel(opt) {
    Emiter.call(this)

    this._container = getElemSafe('container', opt.container)
    // Внешний блок к которому можно добавлять сообщения об ошибках.
    // Нужен для стандартной ситуации - в HTML разметке есть таблица с THEAD и TBODY.
    // TBODY - это контейнер для добавления элементов модели. Добавить к нему DIV с
    // сообщением об ошибке нельзя. Нужна ссылка на TABLE, которую передаем через
    // опцию 'cover'.
    // Если cover не указан, то cover = container
    if (opt.cover) {
        this._cover = getElemSafe('cover', opt.cover)
    } else {
        this._cover = this._container
    }
    this._defaultChild = this._container.firstChild

    this._key = opt.key
    // Для отслеживания уникальности элементов модели. Сигнатура: func(obj) hashcode
    // Хешкод добавляется в _hashcodes
    this._unique = opt.unique
    this._preFunc = opt.preFunc
    
    this._activeClass = opt.activeClass || 'selected'    
    this._errorClass = opt.errorClass || 'errmsg'
    this._messageClass = opt.messageClass

    if (opt.btnRemove) {
        this._btnRemove = getElemSafe('btnRemove', opt.btnRemove)
        
        this._btnRemove.addEventListener('click', () => {
            if (this._removeFunc) {
                this._btnRemove.disabled = true
                this._disableCheckboxes()
                // Удаляем сообщение об ошибке от предыдущего API запроса
                if (this._divRemoveError) {
                    this._divRemoveError.remove()
                }
                this._removing = true
                // удаляем элементы по очереди
                this._removeNext()
            } else {
                this.removeChecked()
            }
        })

        this._isBtnRemoveDisabledByDefault = this._btnRemove.disabled
        this._isBtnRemoveHiddenByDefault = this._btnRemove.hidden

        if (this._isBtnRemoveHiddenByDefault) {
            this.on('checkListChanged', () => {
                if (this._removing) {
                    return
                }
                if (this.countChecked() > 0) {
                    this._btnRemove.hidden = false
                } else {
                    this._btnRemove.hidden = true
                }
            })
        } else if (this._isBtnRemoveDisabledByDefault) {
            this.on('checkListChanged', () => {
                if (this._removing) {
                    return
                }
                if (this.countChecked() > 0) {
                    this._btnRemove.disabled = false
                } else {
                    this._btnRemove.disabled = true
                }
            })
        }
    }

    let ajax = opt.ajax

    if (ajax) {
        this._api = ajax.api
        if (!ajax.api) {
            throw new Error("'ajax.api' option is required")
        } 
        this._listFunc = ajax.listFunc
        this._removeFunc = ajax.removeFunc
        if (this._removeFunc) {
            if (!this._key) {
                throw new Error("'ajax.removeFunc' option is requires 'key' option")
            }
        }
    }

    this._template = new Template({
        id: opt.template.id,
        renders: opt.template.renders,
    });
    
    // Сортировка (опционально)
    let by = opt.sort
    if (by) {
        if (by.key) {
            if (by.reverse) {
                this._cmp = function(a, b) {
                    if (a[by.key] < b[by.key]) {
                        return -1
                    }
                    return 1
                }
            } else {
                this._cmp = function(a, b) {
                    if (a[by.key] < b[by.key]) {
                        return 1
                    }
                    return -1
                }
            }            
        } else if (by.func) {
            if (by.reverse) {
                this._cmp = (a, b) => {
                    return by.func(b, a)
                } 
            } else {
                this._cmp = by.func
            }
        } else {
            if (by.reverse) {
                // 3й кейс - добавляем в начало 
                this._isReverse = true
            }
        }        
    }
    this._arr = []
    this._hashcodes = new Map()
}

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


ListModel.prototype.isAjax = function() {
    return this._api !== undefined
}


ListModel.prototype.disable = function() {
    if (this._btnRemove) {
        this._btnRemove.disabled = true
    }
    this._disableCheckboxes()
}

ListModel.prototype.countChecked = function() {
    let q = 0
    this._arr.forEach((item) => {
        if (item.checkbox && item.checkbox.checked) {
            q++
        }
    })
    return q
}

ListModel.prototype.listChecked = function() {
    let list = []
    this._arr.forEach((item) => {
        if (item.checkbox.checked) {
            list.push(item.obj)
        }
    })
    return list
}

ListModel.prototype.removeChecked = function() {
    while (true) {
        let idx = this._arr.findIndex(item => item.checkbox.checked)
        if (idx == -1) {
            return            
        }
        this.remove(idx)
    }
}

ListModel.prototype._disableCheckboxes = function() {
    this._arr.forEach((item) => {
        item.checkbox.disabled = true
    })
}

ListModel.prototype._enableCheckboxes = function() {
    this._arr.forEach((item) => {
        item.checkbox.disabled = false
    })
}

ListModel.prototype._clear = function() {
    // Для отслеживания уникальности элементов модели (если объявлена unique функция)
    this._hashcodes = new Map()
    this._container.innerHTML = ''
    this._arr = []
    this._selectedItem = null
    if (this._divLoadError) {
        this._divLoadError.remove()
    }
    if (this._divMessage) {
        this._divMessage.remove()
    }
    // Блок с сообщением об ошибке удаления (по API)
    if (this._divRemoveError) {
        this._divRemoveError.remove()
    }
    if (this._btnRemove) {
        this._btnRemove.disabled = this._isBtnRemoveDisabledByDefault
        this._btnRemove.hidden = this._isBtnRemoveHiddenByDefault
    }
}

ListModel.prototype.clear = function() {
    let wasChecked = this.countChecked()
    let selected = this._selectedItem
    this._clear()

    this._setEmptyMessageIfNeeded()

    if (selected) {
        this.emit('select')
    }
    if (wasChecked > 0) {
        this.emit('checkListChanged')
    }
    this.emit('listChanged')
}

ListModel.prototype.setIsDisabledFunc = function(f) {
    this._isDisabledFunc = f
}

ListModel.prototype.setList = function(list) {
    //console.trace()
    // Запоминаем сколько элементов отмечено и выделено, чтобы отправить события
    // замены списка
    let wasChecked = this.countChecked()
    let selected = this._selectedItem
    this._clear()

    if (Array.isArray(list) && list.length > 0) {
        list.forEach(obj => this._add(obj))

        if (this._isDisabledFunc) {
            if (this._isDisabledFunc()) {
                this.disable()
            }
        }
    } else {
        this._setEmptyMessageIfNeeded()
    }
    if (selected) {
        this.emit('select')
    }
    if (wasChecked > 0) {
        this.emit('checkListChanged')
    }
    this.emit('listChanged')
    this.emit('setList')
}

ListModel.prototype._add = function(obj) {
    if (this._preFunc) {
        this._preFunc(obj)
    }
    if (this._arr.length == 0) {
        this._container.innerHTML = ''
    }
    // console.log('model.add:', obj)
    // проверка на уникальность + сохраняем hashcode
    let hashcode;
    if (this._unique) {
        hashcode = this._unique(obj)
        if (this._hashcodes.get(hashcode)) {
            return
        }
    }
    let idx = 0;
    // добавляем в начало массива. 2 кейса: 
    // - нет cmp функции и reverse=true
    // - есть cmp функция, но модель пустая
    if (this._cmp) {
        // ищем индекс, по которому нужно вставить элемент
        for (; idx < this._arr.length; idx++) {
            if (this._cmp(obj, this._arr[idx].obj) == -1) {
                break
            }
        }
    } else if (!this._isReverse) {
        // добавляем в конец массива
        idx = this._arr.length
    }
    let item = this._renderObj(obj)
    if (this._unique) {
        item.hashcode = hashcode
        this._hashcodes.set(hashcode, true)
    }
    // вставляем в массив
    this._arr.splice(idx, 0, item)
    // вставляем в DOM  
    this._container.insertBefore(item.elem, this._container.children[idx])
    return true
}

ListModel.prototype.add = function(obj) {
    //console.log('add to model:', obj)
    let ok = this._add(obj)
    if (ok) {
        this.emit('listChanged')
        return true
    }
}

// keyFunc - return true if found, updateFunc(tags, obj, upd)
ListModel.prototype.partialUpdateBy = function(updateFunc, keyFunc) {
    for (let idx=0; idx<this._arr.length; idx++) {
        let item = this._arr[idx]
        if (keyFunc(item.obj)) {
            updateFunc(item.tags, item.obj)
            return
        }
    }
}

ListModel.prototype.updateBy = function(newobj, keyFunc) {
    for (let idx=0; idx<this._arr.length; idx++) {
        let item = this._arr[idx]
        if (keyFunc(item.obj)) {
            this._update(newobj, item)
            return
        }
    }
}

// fix add silent select after update
ListModel.prototype._update = function(newobj, si) {
    if (this._preFunc) {
        this._preFunc(newobj)
    }

    let hashcode;
    if (this._unique) {
        hashcode = this._unique(newobj)
        if (si.hashcode != hashcode && this._hashcodes.get(hashcode)) {
            // дубликат
            return
        }
    }

    let checked;
    if (si.checkbox) {
        checked = si.checkbox.checked
    }

    let item;
    if (this._cmp) {
        // удаляем
        this._arr = this._arr.filter(item => item != si)
        this._container.removeChild(si.elem)        
        
        let idx = 0
        // ищем индекс, по которому нужно вставить элемент
        for (; idx < this._arr.length; idx++) {
            if (this._cmp(newobj, this._arr[idx].obj) == -1) {
                break
            }
        }
        // рендерим
        item = this._renderObj(newobj, checked)    
        // вставляем в массив
        this._arr.splice(idx, 0, item)
        // вставляем в DOM  
        this._container.insertBefore(item.elem, this._container.children[idx])
    } else {
        // нужно сохранить позицию элемента в списке
        item = this._renderObj(newobj, checked)

        this._container.replaceChild(item.elem, si.elem)

        let idx = this._arr.findIndex(x => x == si)
        this._arr[idx] = item 
    }
    if (this._unique) {        
        item.hashcode = hashcode
        if (hashcode != si.hashcode) {
            // удаляем старый, добавляем новый
            this._hashcodes.delete(si.hashcode)
            this._hashcodes.set(hashcode, true)
        }
    }
    if (si == this._selectedItem) {
        this._selectedItem = item
    }
}

ListModel.prototype.updateSelected = function(newobj) {
    let si = this._selectedItem

    if (!si) {
        return
    }

    this._update(newobj, si)
    
    // /console.log('UPDATED:', this._arr)
    this._selectedItem = null
    this.emit('select')
    this.emit('listChanged')
    return true 
}

// При обновлении элемента модели нужно сохранить галочку checked. 
ListModel.prototype._renderObj = function(obj, checked) {
    let r = this._template.render(obj)
    let item = {
        elem: r.elem, 
        tags: r.tags,
        obj: obj
    }
    // Область для select кликов
    if (r.tags.select) {
        item.select = r.tags.select
        item.select.style.cursor = 'pointer'
        item.select.addEventListener('click', () => this._select(item))
    }

    // Чекбоксы
    let checkbox = r.tags.id 
    if (checkbox) {        
        checkbox.setAttribute('type', 'checkbox')
        if (checked) {
            checkbox.checked = true
        }
        checkbox.addEventListener('change', () => this.emit('checkListChanged'))
        item.checkbox = checkbox
    }
    return item
}


/** 
 * Выделяет текущий элемент по индексу
 * @param {number} idx индекс элемента
 */
ListModel.prototype.select = function(idx) {
    let item = this._arr[idx]
    if (!item) {
        return
    }
    this._select(item)
}

// Выделяет текущий элемент по индексу
ListModel.prototype.unselect = function(silent) {
    if (this._selectedItem) {
        if (this._selectedItem.select) {
            this._selectedItem.select.classList.remove(this._activeClass)
        }
        this._selectedItem = null
        if (!silent) {
            this.emit('select')
        }
    }
}

// Выделяет текущий элемент по индексу
ListModel.prototype.selectBy = function(f) {
    for (let idx=0; idx<this._arr.length; idx++) {
        let item = this._arr[idx]
        if (f(item.obj)) {
            this._select(item)
            return
        }
    }
}

ListModel.prototype.selectByKey = function(id, silent) {
    for (let i=0; i<this._arr.length; i++) {
        let item = this._arr[i]
        if (item.obj[this._key] === id) {
            this._select(item, silent)
            return
        }
    }
}

// Идея метода следующая. Представим левое меню и 2 случая:
// - юзер кликает по пункту, меняется URL, запускается обработчик, 
// который по getSelected берет выделенный элемент и что-то делает
// - юзер после клика обновляет страницу. JS router исходя из адреса
// запускает обработчик. А обработчик должен во первых понять что в модели
// нет выделенных элементов, затем найти в модели нужный элемент по ID 
// (взятому из URL), затем тихо выделить (silent select, без генерации события
// 'select') пункт меню (элемент модели) и в конце что-то сделать с элементом.
// 
// Эти 2 кейса можно реализовать в 1 методе и не усложнять клиентскую логику и код.
// Как работает метод: проверяет есть ли selected элемент, если есть и его ключ
// совпадает с аргументом - возвращает объект. Если selected элемента нет - ищет
// элемент по ключу, тихо выделяет (без генерации 'select' события) и возвращает.
ListModel.prototype.silentSelectByKey = function(id) {
    let obj = this.getSelected()
    if (obj) {
        //console.log('item:', item)
        if (obj[this._key] === id) {
            return obj
        }
    }
    for (let i=0; i<this._arr.length; i++) {
        let item = this._arr[i]
        if (item.obj[this._key] === id) {
            if (this._selectedItem != item) {
                if (this._selectedItem && this._selectedItem.select) {
                    this._selectedItem.select.classList.remove(this._activeClass)
                }

                if (item.select) {
                    item.select.classList.add(this._activeClass)
                }
                this._selectedItem = item
            }
            return item.obj
        }
    }
}

ListModel.prototype._select = function(item, silent) {    
    // клик на любом элементе - снимает выделение с текущего
    if (this._selectedItem && this._selectedItem.select) {
        this._selectedItem.select.classList.remove(this._activeClass)
        if (!silent) {
            this.emit('select', undefined, this._selectedItem.tags)
        }
    }
    
    if (this._selectedItem == item) {
        // Клик на текущем эелементе - снимает выделение
        this._selectedItem = null        
    } else {
        if (item.select) {
            item.select.classList.add(this._activeClass)
        }
        this._selectedItem = item
        if (!silent) {
            this.emit('select', item.obj, item.tags)
        }
    }
}

ListModel.prototype.get = function(idx) {
    let item = this._arr[idx]
    if (item) {
        return item.obj
    }
}

// Выделяет текущий элемент по индексу
ListModel.prototype.getByKey = function(id) {
    for (let i=0; i<this._arr.length; i++) {
        let item = this._arr[i]
        if (item.obj[this._key] === id) {
            return item.obj
        }
    }
}

ListModel.prototype.remove = function(idx) {
    let item = this._arr[idx]
    if (!item) {
        return
    }
    this._container.removeChild(item.elem)
    // удаляем
    this._arr.splice(idx, 1)
    if (this._unique) {
        this._hashcodes.delete(item.hashcode)
    }

    this._setEmptyMessageIfNeeded()
    
    if (this._selectedItem == item) {
        this._selectedItem = null
        this.emit('select')
    }
    if (item.checkbox && item.checkbox.checked) {
        this.emit('checkListChanged')
    }
    this.emit('listChanged')
}

ListModel.prototype.removeSelected = function() {
    let idx = this._arr.findIndex(item => item == this._selectedItem)
    if (idx != -1) {
        this.remove(idx)
    }
}

// функция должна вернуть true. Удялет только 1 элемент
ListModel.prototype.removeBy = function(f) {
    for (let i=0; i<this._arr.length; i++) {
        if (f(this._arr[i].obj)) {
            this.remove(i)
            return
        }
    }
}

ListModel.prototype.getList = function() {
    let arr = []
    this._arr.forEach(item => arr.push(item.obj))
    return arr
}

ListModel.prototype.getSelected = function() {
    if (this._selectedItem) {
        return this._selectedItem.obj
    }
}

ListModel.prototype.size = function() {
    return this._arr.length
}

// FIX  - либо сделать on('setList') либо встроить в setList - selectBy и параметр из URL
// load - отдельный метод load нужен для такого случая - юзер выбирает 
// из выпадающего списка прибор, а для уже для конкретного прибора загружается
// список конфигураций и строится модель
ListModel.prototype.load = function(opt) {
    if (!this._api) {
        //return
        throw new Error('Try load non ajax ListModel')
    }
    this._opt = opt || {}
    // В случае повторных ошибок при попытке загрузить модель - можем получить 
    // список ошибок, поэтому предыдущую удаляем
    if (this._divLoadError) {
        this._divLoadError.remove()
    }
    this._api.req({
        "func": this._opt.listFunc || this._listFunc,
        "data": this._opt.data,
        "onError": (err) => {
            let anchor;
            if (this._divMessage) {
                anchor = this._divMessage
            } else {
                anchor = this._cover
            }
            this._divLoadError = insertError({
                after: anchor, 
                err: err,
                class: this._errorClass
            })
            if (this._opt.onError) {
                this._opt.onError(err)
            }
        },
        "onSuccess": (resp) => {
            this.setList(resp)
            // Success callback нужен для следующего кейса: после загрузки (и 
            // setList !) модели берем ID из URL и делаем selectBy
            if (this._opt.onSuccess) {
                this._opt.onSuccess(resp)
            }
        }
    })
}

// Перезагружаем модель. Зачем нужен такой метод? Простой кейс - после submit формы на
// сервер, Formobj перезагружает привязанную модель. Но, например, в случае добавления
// новой конфигурации к прибору - модель у нас не простая, а привязана к deviceID.
ListModel.prototype.reload = function() {
    this.load(this._opt)
}

ListModel.prototype.setError = function(err) {
    let anchor;
    if (this._divMessage) {
        anchor = this._divMessage
    } else if (this._divLoadError) {
        anchor = this._divLoadError
    } else {
        anchor = this._cover
    }
    return insertError({
        after: anchor, 
        err: err,
        class: this._errorClass
    })
}

ListModel.prototype._removeNext = function() {
    let idx = this._arr.findIndex((item) => item.checkbox.checked)
    if (idx == -1) {
        // не нашли элемент с галочкой - все удалены
        this._whenRemoveEnds()
    } else {
        // ВАЖНО!
        // Вместо id отправляется объект вида {configID: 231}
        //let p = {}
        //p[this._key] = this._arr[idx].obj[this._key]
        let p = this._arr[idx].obj[this._key]
        this._api.req({
            "func": this._removeFunc,
            "data": p,
            "onError": err => {
                this._divRemoveError = insertError({
                    before: this._btnRemove, 
                    err: err,
                    class: this._errorClass
                })
                this._whenRemoveEnds()
            },
            "onSuccess": () => {
                this.remove(idx)
                this._removeNext()
            }
        })
    }
}

ListModel.prototype._whenRemoveEnds = function() {
    this._removing = false
    if (this.countChecked() > 0) {
        // после ошибки могли 
        this._btnRemove.disabled = false        
        this._btnRemove.hidden = false
    } else {
        this._btnRemove.disabled = this._isBtnRemoveDisabledByDefault
        this._btnRemove.hidden = this._isBtnRemoveHiddenByDefault
    }
    this._enableCheckboxes()
    this._setEmptyMessageIfNeeded()
}

ListModel.prototype._setEmptyMessageIfNeeded = function() {
    console.log(this._arr.length, this._defaultChild)
    if (this._arr.length == 0) {
        if (this._defaultChild) {
            this._container.appendChild(this._defaultChild)
        }
    }
}
///////////////

/*
Шаблонизатор нужен в 2 случаях:
- для отрисовки модели
- для построения интерфейса.
    Пример - список конфигурационных параметров с полями ввода



Модель можно сделать отдельной или встроенной в Select, Div-list и т.д.

Методы модели:
- добавить элемент
- удалить элемент
- выделить элемент
- снять выделение
- получить выделенный объект
- сортировка элементов списка?
- добавление / удаление элементов без перерисовки всего списка, чтобы сохранились 
выделения других элементов или взаимодействия с DOM в других элементах ?


Для рендера отдельных элементов в шаблонизаторе нужно реализовать 2 метода:
- рендер, который возвращает отдельно HTMLElement
- рендер, который проходит по списку объектов, для каждого рендерит HTMLElement,
и добавляет в контейнер
*/


function Template(opt) {
    if (opt.id) {
        this._loadTemplate(opt.id)
    } else {
        throw new Error("'id' option is required")
    }
    this._renders = opt.renders || {}    
    this._templateID = opt.id
}

Template.prototype._loadTemplate = function(id) {
    let tag = document.getElementById(id)
    if (!tag) {
        throw new Error(`Template id=${id} not found`)    
    }
    if (tag.tagName != 'TEMPLATE') {
        throw new Error(`Tag id=${id} must be template tag`)    
    }
    return tag.content
}
/*
Template.prototype.fromString = function(templateString) {
    let tag = document.createElement('template')
    tag.innerHTML = templateString
    this._frag = tag.content
    return this
}
*/
Template.prototype.renderTo = function(list, container) {
    container.innerHTML = ''
    if (Array.isArray(list)) {
        list.forEach((item)=>{
            let r = this._render(this._templateID, item)
            container.appendChild(r.elem)
        })
    }
}

// Возвращает объект вида {elem: Element, tags: tags object},
// где elem - это отрендеренный HTML элемент (НЕ DocumentFragment),
// а tags - это карта быстрых ссылок на HTML элементы шаблона, отмеченные
// аттрибутом data-id="name"
Template.prototype.render = function(item) {
    return this._render(this._templateID, item)
}

Template.prototype._render = function(templateID, obj) {
    let getvalue = function(obj, path) {
        let v = obj
        path.split('.').forEach((prop) => {
            if (v === undefined) {
                throw new Error(`Cannot get property '${prop}' of undefined in chain '${path}'`)
            }
            v = v[prop]            
        })
        return v
    }

    let frag = this._loadTemplate(templateID)
    frag = frag.cloneNode(true)
    let elems = frag.querySelectorAll('[data-prop], [data-id], [data-value]')
    let tags = {}

    for (let elem of elems) {
        let id = elem.dataset.id
        let prop = elem.dataset.prop
        let nestedTemplateID = elem.dataset.template
        let value = elem.dataset.value

        if (id) {
            tags[id] = elem
        }

        if (prop) {
            
            if (nestedTemplateID) {
                let list = getvalue(obj, prop)

                //console.log('prop:', prop, '/ nestedTemplateID:', nestedTemplateID, '/ list:', list, ' /elem:', elem)

                if (Array.isArray(list)) {
                    list.forEach((item)=>{
                        let r = this._render(nestedTemplateID, item)
                        elem.appendChild(r.elem)
                    })
                }            
            } else {    
                let x = getvalue(obj, prop)
                if (x !== undefined) {
                    elem.textContent = x
                }
                //console.log('prop:', prop, '/ x:', x, ' /elem:', elem)
            }
        }

        if (value) {
            let x = getvalue(obj, value)
            if (x !== undefined) {
                elem.value = x
            }
        }
    }

    // /console.log(tags, elems, obj)

    let f = this._renders[templateID]
    if (f) {
        f(tags, obj)
    }

    return {
        elem: frag.firstElementChild,
        tags: tags
    }
}


/*
метод onChange нужен чтобы:
- показывать / скрывать кнопку удалить (если выделен или нет элемент списка)
- отображать детали по выделенному элементу. Например, клик по названию в списке - 
в соседней форме отображаем все поля (для редактирования).

Сохранять elem нужно, чтобы убрать activeClass при выборе другого элемента
Сохранять obj нужно чтобы, например, при нажатии кнопки "удалить" знать какой объект
удаляем.
*/




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




/*
Options:
url
data
responseType (по умолчанию 'json')
timeout (milliseconds)
onDone (errorMessage, obj)

data - параметр передается в метод xhr.send() как есть

errorMessage - строка вида "statusCode: statusText" для HTTP кодов отличных
от 200, строка "0: Unknown status" для незавершенных запросов (прерванных по 
таймауту, вызовом метода abort(), из-за сетевой ошибки), строка вида 
"parse json: error message" для ошибки парсинга ответа. 
Если ошибки нет - undefined.

Разделять ошибки на timeout, abort, error (сетевые), как это сделано в jQuery,
нет смысла, ибо JS не дает никакой информации по ошибке кроме вызова 
соответствующего обработчика. А для сетевых ошибок вообще кидает в консоль
исключение, которое нельзя перехватить.

onDone - одна функция вместо двух onError и onSuccess по простой причине - 
большинство ajax запросов - это отправка данных формы. До отправки некоторые 
контролы нужно заблокировать (disabled = true), а после получения ответа от
сервера - разблокировать, причем независимо от результата. 
*/

function ajaxreq(opt) {
    let xhr = new XMLHttpRequest()

    xhr.addEventListener('loadend', function() {
        if (xhr.status != 200) {
            let txt
            if (xhr.status == 0) {
                txt = 'Unknown status'
            } else {
                txt = xhr.statusText
            }
            opt.onDone(xhr.status + ': ' + txt)
        } else {
            let obj
            switch (opt.responseType || 'json') {
            case 'json':
                try {
                    obj = JSON.parse(xhr.responseText)
                } catch (e) {
                    opt.onDone('json parse: ' + e.message)
                    return
                }
                break
            default:
                obj = xhr.responseText
                break
            }
            opt.onDone(undefined, obj)
        }
    })
    xhr.open('POST', opt.url)
    if (opt.timeout) {
        xhr.timeout = opt.timeout
    }
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
    xhr.setRequestHeader('Content-Type', 'text/json; charset=utf-8')
    xhr.send(opt.data)
}



//////////////////// WS

export class WebsocketClient {
    constructor({url, path, reconnectDelay, lostConnectionTimeout, sid}) {
        if (!lostConnectionTimeout) {
            throw new Error('lostConnectionTimeout option is required')
        }
        if (url) {
            this._url = url
        } else if (path) {                
            if (!path.startsWith('/')) {
                throw new Error("path must starts with '/'")
            }
            let u = new URL(location.href)
            this._url = `${u.protocol == 'http:' ? 'ws' : 'wss'}://${u.host}${path}`
        } else {
            throw new Error('url or path option is required')
        }

        this._reconnectDelay = reconnectDelay || 5000
        this._lostConnTimeout = lostConnectionTimeout * 1000
        this._sid = sid
        this._map = new Map()
        this.autoReconnect = true
        this._isConnected = false
        this._funcMap = new Map()
        this._prefixMap = new Map()
    }

    connect() {
        this._ws = new WebSocket(this._url)
        this._ws.onmessage = this._onMessage.bind(this)
        this._ws.onclose = this._onClose.bind(this)
        this._ws.onerror = this._onError.bind(this)
        this._ws.onopen = this._onOpen.bind(this)
    }

    _onOpen() {
        this._isConnected = true
        if (this._sid) {
            this.sendBytes(JSON.stringify(this._sid))
        }
        this._prefixMap.forEach(x => {
            for (let channel of x.channels) {
                this.send('subscribe', channel)
            }
        })
        this._pingIntervalId = setInterval(_ => {
            this.send('ping')
        }, this._lostConnTimeout/2)
        this._resetLostConnTimeout()
        this.onConnected?.()
    }

    _onClose() {
        this._isConnected = false
        this._ws = null
        this.onDisconnected?.()
        if (this.autoReconnect) {		
            this._reconnectTimeoutId = setTimeout(this.connect.bind(this), this._reconnectDelay)
        }
    }

    _onError(err) {
        this._isConnected = false
    }

    _onMessage(e) {
        if (this.onMessage) {
            this.onMessage(e.data)
        } else {
            let envelope;
            try {
                envelope = JSON.parse(e.data)
            } catch (err) {
                throw new Error(`JSON.parse websocket message: ${err}`)
            }
            let func = this._funcMap.get(envelope.type)
            if (func) {
                func(envelope.data)
            }            
        }
        this._resetLostConnTimeout()
    }

    // задает 2 таймера - на отправку пинга и на протухание соединения
    _resetLostConnTimeout() {
        if (this._lostConnTimeoutId) {
            clearTimeout(this._lostConnTimeoutId)
        }
        this._lostConnTimeoutId = setTimeout(_ => {
            if (this._ws) {
                this._ws.close()
            }
        }, this._lostConnTimeout)
    }

    send(messageType, param) {
        if (this._isConnected) {
            this._ws.send(JSON.stringify({
                type: messageType,
                data: param
            }))
        }
    }

    sendBytes(data) {
        if (this._isConnected) {
            this._ws.send(data)
        }
    }

    handle(channelPrefix, handler) {
        if (this._prefixMap.has(channelPrefix)) {
            throw new Error(`channel prefix '${channelPrefix}' already has handler`)
        }
        let funcNames = []
        for (let funcName in handler) {
            if (this._funcMap.has(funcName)) {
                throw new Error(`message type '${funcName}' already has handle func`)
            }
            this._funcMap.set(funcName, handler[funcName])
            funcNames.push(funcName)
        }
        this._prefixMap.set(channelPrefix, {
            channels: [],
            funcNames: funcNames
        })
    }

    unhandle(channelPrefix) {
        let x = this._prefixMap.get(channelPrefix)
        x.funcNames.forEach(funcName => {
            this._funcMap.delete(funcName)
        })
        for (let channel of x.channels) {
            this.send('unsubscribe', channel)
        }
        this._prefixMap.delete(channelPrefix)
    }

    handleFunc(funcName, f) {
        if (this._funcMap.has(funcName)) {
            throw new Error(`message type '${funcName}' already has handle func`)
        }
        this._funcMap.set(funcName, f)
    }

    unhandleFunc(funcName) {
        // fix проверить что удаляем одинокую функцию
        this._funcMap.delete(funcName)
    }

    subscribe(channel) {
        let channelPrefix = channel.split('=')[0]
        let x = this._prefixMap.get(channelPrefix)
        if (!x) {
            throw new Error(`handler for channel prefix ${channelPrefix} is not set`)
        }
        for (let ch of x.channels) {
            if (ch == channel) {
                return
            }
        }
        x.channels.push(channel)
        this.send('subscribe', channel)
    }
    
    unsubscribe(channel) {
        let channelPrefix = channel.split('=')[0]
        let x = this._prefixMap.get(channelPrefix)
        if (!x) {
            throw new Error(`handler for channel prefix ${channelPrefix} is not set`)
        }
        let idx = x.channels.findIndex(ch => ch == channel)
        if (idx >= 0) {
            x.channels.splice(idx, 1)
            this.send('unsubscribe', channel)
        }
    }

    close() {
        this._prefixMap.clear()
        this._funcMap.clear()
        this.autoReconnect = false
        if (this._reconnectTimeoutId) {
            clearTimeout(this._reconnectTimeoutId)
        }
        if (this._lostConnTimeoutId) {
            clearTimeout(this._lostConnTimeoutId)
        }
        if (this._pingIntervalId) {
            clearInterval(this._pingIntervalId)
        }
        if (this._ws) {
            this._ws.close()
        }
    }
}


/*
message вида: 
{
	func: "getAccount",
	param: {
		id: 123
	}
}
*/
//////////////////////////////////////////////////////////////////////////////////

function StatusMessage(opt) {    
    if (!opt.elem) {
        throw new Error("'elem' option is required")
    }
    if (typeof opt.elem == 'string') {
        this._elem = document.getElementById(opt.elem)
        if (!this._elem) {
            throw new Error(`'elem' (id=${opt.elem}) not found`)
        }
    } else {
        this._elem = opt.elem
    }

    this._okClass = opt.okClass || StatusMessage.okClass
    this._errorClass = opt.errorClass || StatusMessage.errorClass
    this._delay = opt.delay || 4000
    this._hide = opt.hide
}

StatusMessage.prototype.flash = function(msg) {
    //console.log('FLASH msg:', msg)

    this._elem.classList.remove(this._errorClass)
    this._elem.classList.add(this._okClass)
    this._elem.textContent = msg
    if (this._timerId) {
        clearTimeout(this._timerId)
    }
    this._timerId = setTimeout(() => {
        if (this._hide) {
            this._elem.style.display = 'none'
        }
        this._elem.textContent = '' 
    }, this._delay)
}

StatusMessage.prototype.error = function(msg) {
    //console.log('ERROR msg:', msg)

    if (this._timerId) {
        clearTimeout(this._timerId)
    }
    this._elem.classList.remove(this._okClass)
    this._elem.classList.add(this._errorClass)    
    this._elem.textContent = msg
    if (this._hide) {
        this._elem.style.display = 'block'
    }
}

StatusMessage.prototype.ok = function(msg) {
    //console.log('OK msg:', msg)
    if (this._timerId) {
        clearTimeout(this._timerId)
    }
    this._elem.classList.remove(this._errorClass)
    this._elem.classList.add(this._okClass)
    this._elem.textContent = msg
    if (this._hide) {
        this._elem.style.display = 'block'
    }
}

StatusMessage.prototype.clear = function() {
    //console.log('CLEAR msg')
    if (this._timerId) {
        clearTimeout(this._timerId)
    }
    if (this._hide) {
        this._elem.style.display = 'none'
    }
    this._elem.textContent = ''        
}

//////////// API

/*
Два типа запросов: которые возвращают данные и запросы действия, которые ничего не
возвращают.

Для первого типа запросов нужно выводить ошибки, для второго типа - ошибки 
и (опционально) сообщение об успехе (типа "параметры обновлены, интерфейсы удалены")
*/


/*
Опции:
- url, адрес API
- timeout, таймаут для ajax запросов
- hideStatus, скрывать status (display: none) в момент отправки запроса или нет (true/false)
- errorClass, css класс для сообщений об ошибке
- okClass, css класс для ok-сообщений
- flash, длительность показа ok-сообщения (миллисекунды). По истечении времени, 
сообщение скрывается. Если hideStatus=true, блок также будет скрыт. 
По умолчанию, ok-сообщение не скрывается.
*/
function API(opt) {
    this._url = opt.url
    this._timeout = opt.timeout
}

/*
Опции:
- status, html элемент для вывода ошибок и ok-сообщений
- ok, сообщение, которое будет выведено в status с стилем okClass, если сервер
вернул что-то кроме ошибки, null.
- empty, сообщение, которое будет показано если сервер вернет null. Например, 
юзер выбрал в списке модем, а на сервере его уже нет. Сервер вернет null, юзер
увидит сообщение.

onError - колбек вызывается в случае ошибки, получает 1 параметр: 
- либо errmsg, строка которая содержит сетевую ошибку либо ошибку парсинга JSON
- либо error объект из ответа сервера. По умолчанию {code: int, message: string}

onSuccess - колбек, вызывается если не было ошибок
*/
API.prototype.req = function(opt) {
    let p = {
        func: opt.func
    }
    if (opt.data !== undefined) {
        p.data = opt.data
    }
    if (opt.status) {
        opt.status.clear()
    }
    ajaxreq({
        "url": this._url,
        "data": JSON.stringify(p),
        "timeout": this._timeout,
        "onDone": (errmsg, resp) => {
            if (errmsg) {
                if (opt.status) {
                    opt.status.error(errmsg)
                } else {
                    console.log('ajaxreq:', errmsg)
                }
                if (opt.onError) {
                    opt.onError(errmsg)
                }
                return
            }
            if (resp && resp.error) {
                if (opt.status) {
                    opt.status.error(resp.error.message)
                } else {
                    console.log('ajaxreq resp:', resp.error.message)
                }
                if (opt.onError) {
                    opt.onError(resp.error)
                }
                return
            }
            if (opt.onSuccess) {
                opt.onSuccess(resp)
            }
        }
    })
}


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

function Emiter() {
    this._emit_map = {}
}

Emiter.prototype.on = function(event, func) {
    let arr = this._emit_map[event]
    if (arr) {
        arr.push(func)
    } else {
        this._emit_map[event] = [func]
    }
}

Emiter.prototype.emit = function(event, ...obj) {
    let arr = this._emit_map[event]
    if (arr) {
        arr.forEach((func) => {
            func(...obj)
        })
    }
}


/////////////////// SELECT

//function ExistingSelect(opt) {
    //thi
//}


// Событие 'select', обработчик получает на вход ID 
function Select(opt) {
    Emiter.call(this)

    if (!opt.field) {
        throw new Error("'field' option is required")
    }
    this._select = opt.field
    /*
    if (typeof opt.select == 'string') {
        this._select = document.getElementById(opt.select)
        if (!this._select) {
            throw new Error(`'elem' (id=${opt.select}) not found`)
        }
    } else {
        this._select = opt.select
    }
    */

    //if (!opt.key) {
    //    throw new Error("'key' option is required")
    //}
    this._select.addEventListener('change', this._onchange.bind(this))
    this._arr = []
    this._key = opt.key
    this._noZero = opt.noZero
    this._errorClass = opt.errorClass || 'errmsg'

    if (opt.name) {
        if (typeof opt.name == 'string') {
            this._getName = (item) => {
                return item[opt.name]
            }
        } else {
            // fix function
            this._getName = opt.name
        }
    } else {
        this._getName = (item) => {
            return item
        }
    }


    if (opt.filled) {
        for (let i=0; i<this._select.options.length; i++) {
            let option = this._select.options[i]
            if (option.value !== '') {
                let v;
                let value = option.value.trim()

                if (opt.valueType) {
                    switch (opt.valueType) {
                    case 'int':
                        v = parseInt(value)
                        if (!isNaN(v)) {
                            this._arr.push(v)
                        }
                        break
                    case 'float':
                        v = parseFloat(value)
                        if (!isNaN(v)) {
                            this._arr.push(v)
                        }
                        break
                    default:
                        throw new Error(`Unknown valueType '${opt.valueType}'`)
                    }
                } else {
                    this._arr.push(value)
                }              
            }
        }
    }

    //console.log('filled:', this._arr)
}

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

Select.prototype.getActionType = function() {
    return 'select'
}

Select.prototype.getType = function() {
    return 'select'
}

Select.prototype.enable = function() {
    this._select.disabled = false
}

Select.prototype.disable = function() {
    this._select.disabled = true
}

Select.prototype.getAnchor = function() {
    return this._select
}

Select.prototype.getCover = function() {
    return this._select
}

Select.prototype.setList = function(list) {
    //console.log('SET LIST select:', list)    
    this._select.innerHTML = ''
    if (Array.isArray(list)) {
        if (!this._noZero) {
            // добавляем пустой option
            this._select.appendChild(document.createElement('option'))
        }
        // заполняем данными
        this._arr = list
        this._arr.forEach((item) => {
            let option = document.createElement('option')
            if (this._key) {
                option.value = item[this._key]
            } else {
                option.value = item
            }
            option.textContent = this._getName(item)
            this._select.appendChild(option)
        })
        this.emit('listChanged')
    }
}

Select.prototype.clear = function() {
    //console.log('CLEAR select:', this._select) 
    this._select.innerHTML = ''
    this._arr = []
    if (this._divLoadError) {
        this._divLoadError.remove()
    }
    this.emit('listChanged')
}

Select.prototype.likeReset = function() {
    this.emit('select')
}

Select.prototype.reset = function() {
    //console.log('RESET select:', this._select)    
    if (this._arr.length == 0) {
        return
    }
    if (this._noZero) {
        let item = this._arr[0]
        if (this._key) {
            this._select.value = item[this._key]
        } else {
            this._select.value = item
        }
    } else {
        this._select.value = ""        
    }
    this._select.dispatchEvent(new Event('change'))
}

Select.prototype.getSelected = function() {
    //console.log(this._select.selectedIndex)
    let idx = this._select.selectedIndex
    // индекс 0 - это пустой option, поэтому idx-1
    if (!this._noZero) {
        idx--
    }
    return this._arr[idx]
}
/*
Select.prototype.setError = function(err) {
    let div;
    if (this._divLoadError) {
        div = insertError({
            after: this._divLoadError, 
            err: err,
            class: this._errorClass
        })
    } else {
        div = insertError({
            after: this._select, 
            err: err,
            class: this._errorClass
        })
    }
    return div
}
*/
Select.prototype._onchange = function() {
    //console.log('on change:')
    //this.emit('select', this.getSelected())
    let item = this.getSelected()
    if (!item) {
        this.emit('select')
    } else if (this._key) {
        this.emit('select', item[this._key])
    } else {
        this.emit('select', item)
    }    
}

Select.prototype.select = function(id) {
    //console.log('SELECT select:', id)    
    //console.log("select.select:", this._select.value, id)
    this._select.value = id
    this._select.dispatchEvent(new Event('change'))
}

Select.prototype.getList = function() {
    return this._arr
}

////////// RADIO LIST

function RadioList(opt) {
    Emiter.call(this)
/*
    let required = ['templateId', 'container', 'key']

    required.forEach((option) => {
        if (!opt[option]) {
            throw new Error(`'${option}' option is required`)
        }
    })
    */

    this._errorClass = opt.errorClass || 'errmsg'
    this._setEmptyMessage = opt.setEmptyMessage

    if (opt.templateId) {
        let renders = {}
        renders[opt.templateId] = (tags) => {
            if (!tags.radio) {
                return
            }
            tags.radio.setAttribute('type', 'radio')
            let idx = this._buttons.length
            this._buttons.push(tags.radio)
            tags.radio.addEventListener('click', () => this._onclick(idx))
        }
        this._template = new Template({
            id: opt.templateId,
            renders: renders,
        })
    } else {

    }

    this._container = getElemSafe('container', opt.container)
    if (opt.cover) {
        this._cover = getElemSafe('cover', opt.cover)
    } else {
        this._cover = this._container
    }

    this._key = opt.key
    this._buttons = []
    this._arr = []
}

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

RadioList.prototype.getActionType = function() {
    return 'select'
}

RadioList.prototype.getType = function() {
    return 'select'
}

RadioList.prototype.getAnchor = function() {
    return this._cover
}

RadioList.prototype.getCover = function() {
    return this._cover
}

RadioList.prototype.disable = function() {
    //console.log('DISABLE RADIO', this._buttons)
    this._buttons.forEach((radio) => {
        radio.disabled = true
    })
}

RadioList.prototype.enable = function() {
    //console.log('ENABLE RADIO', this._buttons)
    this._buttons.forEach((radio) => {
        radio.disabled = false
    })
}

RadioList.prototype.clear = function() {
    //console.log('CLEAR radio:') 
    this._currentIdx = undefined
    this._container.innerHTML = ''
    this._buttons = []
    this._arr = []
    if (this._messageDiv) {
        this._messageDiv.remove()
    }
    if (this._divLoadError) {
        this._divLoadError.remove()
    }
}

RadioList.prototype.setList = function(list) {
    //console.log('setList radio:', list)
    this.clear()
    if (Array.isArray(list)) {
        this._arr = list
        this._template.renderTo(list, this._container)
    } else {
        if (this._setEmptyMessage) {
            let div = document.createElement('div')
            div.textContent = this._setEmptyMessage

            this._messageDiv = div

            this._cover.parentNode.insertBefore(div, this._cover.nextSibling)
        }
    }
}

RadioList.prototype.getList = function() {
    return this._arr
}

RadioList.prototype.select = function(id) {
    //console.log('SELECT radio:', id, this._key, this._arr)
    //console.log(this._arr)
    this._arr.forEach((obj, idx) => {
        if (id === obj[this._key]) {
            this._onclick(idx)
            return
        }
    })    
}

RadioList.prototype.getSelected = function() {
    if (this._currentIdx !== undefined) {
        return this._arr[this._currentIdx]
    }
}


/*
RadioList.prototype.setError = function(err) {
    return insertError({
        after: this._cover, 
        err: err,
        class: this._errorClass
    })
}
*/

RadioList.prototype._onclick = function(idx) {
    //console.log('radioList select:', idx, this._currentIdx, this._buttons)
    let radio = this._buttons[idx]
    let obj = this._arr[idx]
    let id = obj[this._key]

    if (this._currentIdx === idx) {
        // снимаем отметку
        radio.checked = false
        this._currentIdx = undefined
        this.emit('select')
    } else {
        let prev = this._buttons[this._currentIdx]
        if (prev) {
            prev.checked = false
        }
        radio.checked = true
        this._currentIdx = idx
        this.emit('select', id)
    }
}
/////////////////////////


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

    this._key = opt.key
    this._name = opt.name
    this._input = getElemSafe('input', opt.input)
    //this._input.addEventListener('input', () => this.emit('changed'))
    this._errorClass = opt.errorClass || 'errmsg'
}

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

ReadonlyInput.prototype.getActionType = function() {
    return 'input'
}

ReadonlyInput.prototype.getType = function() {
    return 'input'
}

ReadonlyInput.prototype.disable = function() {
    this._input.disabled = true
}

ReadonlyInput.prototype.enable = function() {
    this._input.disabled = false
}

ReadonlyInput.prototype.reset = function() {
    this._input.value = undefined
    this.emit('change')
}

ReadonlyInput.prototype.clear = function() {
    this._input.value = ''
    this._value = undefined
    this.emit('change')
}

ReadonlyInput.prototype.getValue = function() {
    return this._value
}

ReadonlyInput.prototype.setValue = function(value) {
    // console.log('setValue:', value)
    if (value === undefined) {
        this._value = undefined
        this._input.value = ''
        this.emit('change')
    } else {
        this._value = value
        if (this._key) {        
            this.emit('change', value[this._key])
        } else {        
            this.emit('change', value)
        }
        if (this._name) {
            this._input.value = value[this._name]
        } else {
            this._input.value = value
        }
    }
}

ReadonlyInput.prototype.getAnchor = function() {
    return this._input
}

/*
ReadonlyInput.prototype.setError = function(err) {
    return insertError({
        after: this._input, 
        err: err,
        class: this._errorClass
    })
}
*/

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

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

    if (!opt.templateId) {
        throw new Error("option 'templateId' is required")
    }
    let renders = {}
    renders[opt.templateId] = (tags) => {
        if (!tags.input) {
            return
        }
        this._inputs.push(tags.input)
        tags.input.addEventListener('input', () => {
            
            this.emit('changed')
        })
    }
    this._template = new Template({
        id: opt.templateId,
        renders: renders,
    })
    
    this._container = getElemSafe('container', opt.container)
    if (opt.cover) {
        this._cover = getElemSafe('cover', opt.cover)
    } else {
        this._cover = this._container
    }
    this._errorClass = opt.errorClass || 'errmsg'
    this._inputs = []

    this._setEmptyMessage = opt.setEmptyMessage
}

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

InputList.prototype.getType = function() {
    return 'list'
}


InputList.prototype.disable = function() {
    this._inputs.forEach((input) => {
        input.disabled = true
    })
}

InputList.prototype.enable = function() {
    this._inputs.forEach((input) => {
        input.disabled = false
    })
}


InputList.prototype.reset = function() {
    // FIX
}

InputList.prototype.clear = function() {
    this._container.innerHTML = ''
    this._inputs = []
    if (this._messageDiv) {
        this._messageDiv.remove()
    }
}

InputList.prototype.setList = function(list) {
    //console.log('INPUT LIST: setList', list)    
    if (Array.isArray(list)) {
        this._template.renderTo(list, this._container)
    } else {
        if (this._setEmptyMessage) {
            let div = document.createElement('div')
            div.textContent = this._setEmptyMessage

            this._messageDiv = div

            this._cover.parentNode.insertBefore(div, this._cover.nextSibling)
        }
    }
}

InputList.prototype.getList = function() {
    //console.log('getList')
    return []
}

InputList.prototype.getValues = function() {
    //console.log('getValues')
    let arr = []
    this._inputs.forEach((input) => {
        arr.push(input.value)
    })
    return arr
}

InputList.prototype.setValues = function(values) {
    //console.log('setValues')
    this._inputs.forEach((input, idx) => {
        input.value = values[idx]
    })
}

/*
InputList.prototype.setError = function(err, idx) {
    let anchor = this._inputs[idx]
    return insertError({
        after: anchor, 
        err: err,
        class: this._errorClass
    })
}
*/

InputList.prototype.getAnchor = function(idx) {
    if (idx !== undefined ) {
        return this._inputs[idx] // fix out of range
    }
    return this._cover
}

InputList.prototype.getCover = function() {
    return this._cover
}

////////// CHECK LIST

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

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

    if (!opt.templateId) {
        throw new Error("option 'templateId' is required")
    }
    let renders = {}
    renders[opt.templateId] = (tags) => {
        if (!tags.checkbox) {
            return
        }
        tags.checkbox.setAttribute('type', 'checkbox')
        let idx = this._buttons.length
        this._buttons.push(tags.checkbox)
        tags.checkbox.addEventListener('click', () => this._onclick(idx))
    }
    this._template = new Template({
        id: opt.templateId,
        renders: renders,
    })

    this._container = getElemSafe('container', opt.container)
    if (opt.cover) {
        this._cover = getElemSafe('cover', opt.cover)
    } else {
        this._cover = this._container
    }

    if (!opt.key) {
        throw new Error("option 'key' is required")
    }

    this._key = opt.key
    this._buttons = []
    this._arr = []
}

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

CheckList.prototype.getType = function() {
    return 'list'
}

CheckList.prototype.getActionType = function() {
    return 'input'
}


CheckList.prototype.disable = function() {
    //console.log('DISABEL RADIO', this._buttons)
    this._buttons.forEach((c) => {
        c.disabled = true
    })
}

CheckList.prototype.enable = function() {
    //console.log('DISABEL RADIO', this._buttons)
    this._buttons.forEach((c) => {
        c.disabled = false
    })
}

CheckList.prototype.reset = function() {
    this._buttons.forEach((checkbox) => {
        checkbox.checked = false
    })
}

CheckList.prototype.clear = function() {
    //console.log('CLEAR radio:')    
    this._currentIdx = undefined
    this._container.innerHTML = ''
    this._buttons = []
    this._arr = []

    if (this._messageDiv) {
        this._messageDiv.remove()
    }
    if (this._divLoadError) {
        this._divLoadError.remove()
    }
}

CheckList.prototype.setList = function(list) {
    
    this.clear()
    if (Array.isArray(list)) {
        this._arr = list
        this._template.renderTo(list, this._container)
    } else {
        if (this._setEmptyMessage) {
            let div = document.createElement('div')
            div.textContent = this._setEmptyMessage

            this._messageDiv = div

            this._container.parentNode.insertBefore(div, this._container.nextSibling)
        }
    }
}

CheckList.prototype.getList = function() {
    return this._arr
}

CheckList.prototype.setValues = function(ids) {
    console.log('SET VALUES:', ids, this._arr)
    if (!Array.isArray(ids)) {
        return
    }
    ids.forEach(id => {
        this._arr.forEach((obj, idx) => {
            if (obj[this._key] === id) {
                this._buttons[idx].checked = true
            }
        })
    })
    this.emit('changed')
}

CheckList.prototype.getValues = function() {
    let r = []
    this._arr.forEach((obj, idx) => {
        if (this._buttons[idx].checked) {
            r.push(obj[this._key])
        }
    })
    console.log('GET VALUES:', r)
    return r
}
// getAnchor - возвращает HTML Element, к которому можно привязать ошибку.
// Если idx не указан или ошибочный чекбокс не будет найден - вернет ссылку на cover. 
// Иначе вернет ссылку на родительский элемент для ошибочного чекбокса, предполагается 
// что это будет LABEL.
// Idx считается только для отмеченных чекбоксов. То есть idx=1 будет указывать на 2й
// отмеченный чекбокс.
CheckList.prototype.getAnchor = function(idx) {
    if (idx === undefined) {
        return this._cover
    }
    let checkedIdx = 0
    for (let i=0; i<this._buttons.length; i++) {
        let checkbox = this._buttons[i]
        if (checkbox.checked) {
            if (idx === checkedIdx) {
                return checkbox.parentNode
            }
            checkedIdx++    
        }        
    }
    return this._cover
}

/*
CheckList.prototype.setError = function(err, idx) {
    if (idx !== undefined) {
        // ссылка на label, а не checkbox
        let elem = this._buttons[idx].parentNode
        return insertError({
            after: elem, 
            err: err,
            class: this._errorClass
        })
    }
    return insertError({
        after: this._cover, 
        err: err,
        class: this._errorClass
    })
}
*/

CheckList.prototype.getCover = function() {
    return this._cover
}

CheckList.prototype._onclick = function(idx) {
    this.emit('changed')
}

///////////////////// URL

// Добавляем в URL переменную, не трогая остальные
function setParamToURL(name, value) {
    // по умолчанию state = null
    if (history.state) {
        // Позволяет устранить проблему: когда юзер выбирает пункт в select и когда 
        // программа выбирает пункт при обработке popstate события (юзер нажал 
        // кнопку Назад) - происходит зацикливание истории (кнопка назад срабатывает 
        // 1 раз и затем страница не меняется).
        if (history.state[name] === value) {
            return
        }
    }
    const u = new URL(location.href)
    let usp = new URLSearchParams(u.searchParams)
    if (value === undefined) {
        // например, выбрали в select пустой элемент
        usp.delete(name)
    } else {
        usp.set(name, value)
    }
    let state = {}
    state[name] = value
    // если склеивать path без условных проверок - получим путь вида /configs?
    // при сбросе параметра (value = undefined)
    let path = u.pathname
    const searchParams = usp.toString()
    if (searchParams !== '') {
        path += '?' + searchParams
    }
    path += u.hash
    history.pushState(state, "", path)
}

function getParamFromURL(name, dataType) {
    const u = new URL(location.href)
    let usp = new URLSearchParams(u.searchParams)
    if (!dataType) {
        dataType = 'str'
    }
    let v;
    switch (dataType) {
        case 'str':
            v = usp.get(name) || ''
            break
        
        case 'int':
            v = usp.get(name)
            v = parseInt(v) || 0
            break

        case 'float':
            v = usp.get(name)
            v = parseFloat(v) || 0

        case 'bool':
            v = usp.get(name)
            if (v == 'on') {
                v = true
            } else {
                v = false
            }
            break

        default:
            throw new Error(`Unknown type: ${dataType}`)
    }
    return v
}

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

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 insertMessage(opt) {
    let div = document.createElement('div')
    if (opt.class) {
        div.className = opt.class
    }
    div.textContent = opt.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 getElemSafe(name, opt) {
    if (opt instanceof Element) {
        return opt
    }
    if (typeof opt == 'string') {
        let elem = document.getElementById(opt)
        if (!elem) {
            throw new Error(`'${name}' option not found in DOM by id=${opt}`)
        }
        return elem
    }
    throw new Error(`'${name}' option must be Element or id string, not ${JSON.stringify(opt)}`)
}

/*
1. Все параметры создаются вместе с конфигом через админку http://167.99.157.181:8081/configs в разделе Field Set Configs.
2. Конфиг и параметры редактировать нельзя,  Вы можете привязыватся к имени параметра без проблем. 
После создания имя изменить нельзя, конфиг (таблица configs) отредактировать нельзя. 
Если создан конфиг, например, с 3 параметрами, имена и количество

Создавать таблицу где имя параметра будет именем колонки?
Если я буду программно добавлять/удалять колонки (вместо строк) - как-то нереляционно.
Если вы предполагаете что все параметры можно предусмотреть заранее - это ошибка. 
Завтра будут новые приборы, новые конфиги и новые параметры. Делать для каждого 
нового параметра вручную ALTER TABLE ADD COLUMN - неправильно.
*/



