@tool
extends Panel

#region Определяемое при использовании
## Индекс -> Имя устройства. Нужен для предсказуемого
## порядка отображения: верх->вниз или лево->право.
## Имя устройства должно быть взято из settings.gd
@export var index_to_unit_names: Array[StringName]
## Минимальная частота, МГЦ
@export var freq_min: float
## Максимальная частота, МГЦ
@export var freq_max: float
## Цвет диапазона если включен (разрешён)
@export_color_no_alpha var color_enabled: Color
## Цвет диапазона если выключен (запрещён)
@export_color_no_alpha var color_disabled: Color
## Прозрачность диапазон на шкале (диапазоны могут накладываться, что бы было видно наложения)
@export_range(0, 1.0) var color_alpha: float
## Цвет нечётных строк шкалы
@export_color_no_alpha var color_odd: Color
## Цвет чётных строк шкалы
@export_color_no_alpha var color_even: Color
## Индекс колонки в таблице для разрешения работы приёмника
@export var column_cmd_rx: int
## Индекс колонки в таблице для разрешения работы передатчика
@export var column_cmd_tx: int
## Максимальное количесво кластеров на одно устройство
@export var max_clasters_per_unit: int
## Сообщение пользователю
@export var msg_select_unit: String
## Сообщение пользователю
@export var msg_waiting_data: String
#endregion

#region Загружаемые ресурсы
@export_file('*.tscn') var table_headers: Array[String]
@export_file('*.tscn') var table_cells: Array[String]
@export_file('*.png') var image_file_rx: String
@export_file('*.png') var image_file_tx: String
@export var table_rows_hight: int
@export var table_column_widths: Array[int]
@export var table_header_widths: Array[int]
@export var table_header_text: Array[String]
#endregion

#region Заполняется автоматически при запуске
var name_to_index: Dictionary[StringName, int]  ## Имя устройства -> Индекс
var name_to_scale: Dictionary[StringName, GridContainer]  ## Имя устройства -> Шкала
var table_cell_row: Array
#endregion

#region Заполняется во время работы
var yems_info: Dictionary[StringName, Dictionary]  ## Доклады
var yems_cmd: Dictionary[StringName, Dictionary]   ## Команды
## Порядок следования кластеров в таблице.
## {имя_устройства: { имя_кластера: индекс_в_таблице } }
var cnames_table: Dictionary[StringName, Dictionary]
## Порядок следования кластеров на шкале.
## {имя_устройства: { индекс_на_шкале } }
var pnames_scale: Dictionary[StringName, int]
## Текущее выбранное оператором устройство на шкале
var selected_unit: Dictionary[StringName, bool]
## Состояния кластеров в соответствии с докладами и командами
var cmd_vs_info: Dictionary[StringName, Dictionary]
#endregion


## Состояние кластера
enum ClasterStatus {
    DELETED,
    RUNNING
}


## Режим управления кластером
enum ClasterMode {
    MANUAL,  ## Ручной, управляется операторром
    AUTO,    ## Автоматический, управляется программой
    IGNORE   ## Игнорируемый, не управляется
}


## Состояние кластера. Значения должны соответствовать состояниям [b]six-state-button[/b]
enum ClasterState {
    WAITING = 0,   ## Ожидание даннных
    DONE = 1,      ## Выполнено
    MISSMATCH = 2  ## Несоответствие
}

func _ready() -> void:
    # Инициализировать переменные
    color_enabled.a = color_alpha
    color_disabled.a = color_alpha
    for i in index_to_unit_names.size():
        var nm: = index_to_unit_names[i]
        name_to_index[nm] = i
        yems_info[nm] = {}
        yems_cmd[nm] = {}
        cnames_table[nm] = {}
        selected_unit[nm] = false
        cmd_vs_info[nm] = {}
    var btns_scales = get_tree().get_nodes_in_group('кнопки-шкалы')
    for node in btns_scales:
        var nm: = node.get_meta('unit_name') as StringName
        node.connect('toggled', _on_scale_selected.bind(nm))
    var claster_scales = get_tree().get_nodes_in_group('шкалы-кластеров')
    for node in claster_scales:
        var nm: = node.get_meta('unit_name') as StringName
        name_to_scale[nm] = node

    # Расскрасить чётные-нечётные
    for node in get_tree().get_nodes_in_group('нечётные'):
        node.get_node('panel_rx').back_color = color_odd
        node.get_node('panel_tx').back_color = color_odd
    for node in get_tree().get_nodes_in_group('чётные'):
        node.get_node('panel_rx').back_color = color_even
        node.get_node('panel_tx').back_color = color_even

    # Подключить обработчик для приёма данных
    for nm in yems_info:
        var yems_unit = network.get_unit_instance(nm)
        yems_unit.connect('data_received', on_data_received.bind(nm))

    # Настроить таблицу
    var header_row: = []
    for cell_scn in table_headers:
        header_row.append(load(cell_scn))
    for cell_scn in table_cells:
        table_cell_row.append(load(cell_scn))
    var ctable: = $'таблица/сетка'
    ctable.set_header(header_row)
    ctable.set_header_text(table_header_text)
    ctable.set_header_property(6, 'image_file', image_file_rx)
    ctable.set_header_property(7, 'image_file', image_file_tx)
    ctable.set_headers_min_size(table_header_widths)
    ctable.set_columns_min_size(table_column_widths)
    ctable.set_rows_min_hight(table_rows_hight)
    ctable.set_header_min_hight(table_rows_hight)
    ctable.pin_header2()


## Вызывается при выборе оператором устройства на шкале
## [param nm] - имя выбранного устройства
func _on_scale_selected(val: bool, nm: StringName):
    selected_unit[nm] = val
    if not val:
        $'таблица/сетка'.clear_rows()
        cnames_table[nm].clear()
        $label_msg.text = msg_select_unit
        $label_msg.show()
        return
    var unit_info = yems_info[nm]
    if not unit_info: # проверить, есть ли данные
        $label_msg.text = msg_waiting_data
        return  # данных ещё нет, ничего не далать
    $label_msg.hide()
    var ctable = $'таблица/сетка'
    var indexes = cnames_table[nm]
    var unit_cvi = cmd_vs_info[nm]
    map_yems_data_to_table(unit_info, ctable, indexes)
    connect_cmd_handlers(ctable, nm, indexes)
    update_cmd_vs_info(nm)
    map_cvi_to_table(unit_cvi, ctable, indexes)


func map_cvi_to_table(unit_cvi: Dictionary, ctable: GDTable, indexes: Dictionary):
    for cname in indexes:
        var i_row = indexes[cname]
        var state_rxe: = ClasterState.WAITING
        var state_txe: = ClasterState.WAITING
        if unit_cvi.has(cname):
            state_rxe = unit_cvi[cname]['state_rxe']
            state_txe = unit_cvi[cname]['state_txe']
        ctable.set_node_property(column_cmd_rx, i_row, 'state', int(state_rxe))
        ctable.set_node_property(column_cmd_tx, i_row, 'state', int(state_txe))


func update_cmd_vs_info(nm: StringName):
    if yems_info.has(nm):
        return
    var unit_info = yems_info[nm]
    if not unit_info.has('mcls'):
        return
    if not yems_cmd.has(nm):
        return
    var unit_cvi = {}
    for cls_info in unit_info.mcls:
        var claster_cvi = {}
        claster_cvi['state_txe'] = ClasterState.WAITING
        claster_cvi['state_rxe'] = ClasterState.WAITING
        var unit_cmd = yems_cmd[nm]
        if not unit_cmd.has(cls_info.cname):
            continue
        var cls_cmd = unit_cmd[cls_info.cname]
        claster_cvi['state_rxe'] = ClasterState.DONE if cls_info.rxe == cls_cmd.rxe else ClasterState.MISSMATCH
        claster_cvi['state_txe'] = ClasterState.DONE if cls_info.txe == cls_cmd.txe else ClasterState.MISSMATCH
        unit_cvi[cls_info.cname] = claster_cvi
    cmd_vs_info[nm] = unit_cvi


## Подключает обработчики команд к таблице кластеров
## [param cls_table] - таблица для отображения кластеров
## [param nm] - имя устройства
func connect_cmd_handlers(cls_table: GDTable, nm: StringName, indexes: Dictionary):
    var rows_count = cls_table.get_rows_count()
    if indexes.size() != rows_count:
        push_error('indexes.size() != rows_count')
        return
    for cname in indexes:
        # Имя устройства и имя кластера
        var i_row = indexes[cname]
        # Подключить обработчки разрешения приёмников
        var node = cls_table.get_node2(column_cmd_rx, i_row)
        node.connect_check('toggled', _on_user_cmd.bind(nm, cname, 'rxe'))
        # Подключить обработчки разрешения приёмников
        node = cls_table.get_node2(column_cmd_tx, i_row)
        node.connect_check('toggled', _on_user_cmd.bind(nm, cname, 'txe'))


func _on_user_cmd(val: bool, nm: StringName, cname: String, param_name: StringName):
    var unit_cmd = yems_cmd[nm]
    if cname in unit_cmd:
        unit_cmd[cname][param_name] = val
    else:
        unit_cmd[cname] = {param_name: val}


## Обрабатывает событие [b]data_received[/b]
func on_data_received(data: Dictionary, nm: StringName):
    var unit_data: = yems_info[nm]
    unit_data.merge(data, true)
    if not unit_data:
        return
    var ctable: GDTable = $'таблица/сетка'
    var claster_scale: = name_to_scale[nm] as GridContainer
    var indexes: = cnames_table[nm] # Индексы строк в таблице
    if selected_unit[nm]:
        map_yems_data_to_table(unit_data, ctable, indexes)
    map_yems_data_to_scale(unit_data, claster_scale)
    var inters: = find_inters(yems_info)
    var unit_cvi = cmd_vs_info[nm]
    reset_matrix()
    map_inters_to_matrix(inters, $'матрица')
    set_timeouts(yems_info)
    cmd_to_unit(nm)
    update_cmd_vs_info(nm)
    map_cvi_to_table(unit_cvi, ctable, indexes)


func cmd_to_unit(nm: StringName):
    var unit_yemstg: = network.get_unit_instance(nm)
    if unit_yemstg.cmd_state != unit.Unit.CmdState.DONE:
        return
    # Команда формируется из докладов (данных принятых из сети от устройств)
    var cmd = yems_info[nm]
    # Переписать новыми значениями из команды заданной оператором
    cmd['mtype'] = 'cmd'
    var unit_cmd = yems_cmd[nm]
    for cls_info in cmd.mcls:
        if unit_cmd.has(cls_info.cname):
            var cmd_cls = unit_cmd[cls_info.cname]
            cls_info.merge(cmd_cls, true)
    unit_yemstg.cmd.merge(cmd, true)
    unit_yemstg.cmd_state = unit.Unit.CmdState.SEND


func set_timeouts(info: Dictionary):
    for nm in info: # nm - имя устройства
        var unit_data = info[nm]
        if not unit_data: continue
        for cls in unit_data.mcls: # cls - один кластер
            var timer_name = NodePath('%s_%s' % [nm, cls.cname])
            var timer = get_node_or_null(timer_name)
            if not timer:
                timer = Timer.new()
                timer.name = timer_name
                timer.one_shot = true
                timer.connect('timeout', _on_timeout_ttl.bind(nm, cls.cname))
                add_child(timer)
            timer.start(cls.ttl)
            cls['status'] = ClasterStatus.RUNNING


## Обработчик истечения времени жизни кластера
func _on_timeout_ttl(nm, cname):
    var timer_name = NodePath('%s_%s' % [nm, cname])
    var timer = get_node_or_null(timer_name)
    if timer:
        remove_child(timer)
        timer.queue_free()
    var unit_data = yems_info[nm]
    for cls in unit_data.mcls:
        if cname == cls.cname:
            cls.status = ClasterStatus.DELETED


func reset_matrix():
    var nodes = get_tree().get_nodes_in_group('матрица-к')
    for node in nodes:
        node.button_pressed = false


## Отображает пересечения на матрице
func map_inters_to_matrix(inters: Array, matrix: GridContainer) -> void:
    var nodes = matrix.get_children()
    for item in inters:
        var tx_unit_name = item[0]
        var rx_unit_name = item[1]
        var i_col = 1 + name_to_index[tx_unit_name] # (1 +) - одна колонка для значков приёмников
        var i_row = 1 + name_to_index[rx_unit_name] # (1 +) - один ряд для значков передатчиков
        var idx = i_col + i_row * matrix.columns  # последовательный индекс в GridContainer
        var node = nodes[idx] # Искомый элемент
        node.button_pressed = true


## Отображает кластеры в таблицу
## [param nm] - имя устройства
func map_yems_data_to_table(unit_data: Dictionary, ctable: GDTable, indexes: Dictionary):
    # Обойти все кластеры устройства
    for cls in unit_data.mcls:
        # Строки для заполнения ряда
        var row_text = [cls.cname, 'Активен', '%.1f' % cls.ttl, '%.1f' % cls.daa, '%.1f' % cls.dap, '%.1f-%.1f' % [cls.cfb - cls.wfb / 2.0, cls.cfb + cls.wfb / 2.0]]
        # Если такой кластер уже есть, обновить его строку в таблице
        if cls.cname in indexes:
            # Получить индекс строки кластера в таблице
            var i_row = indexes[cls.cname]
            # Установить новый текст
            ctable.set_row_text(i_row, row_text)
        # Если такого кластера нет, то создать его строку в таблице
        else:
            # Добавить новый ряд
            ctable.add_row(table_cell_row)
            # Получить индекс нового ряда в таблице
            var i_row: = indexes.size()
            # Записать индекс нового ряда (связать с именем кластера)
            indexes[cls.cname] = i_row
            # Установить высоту ряда (можно пропустить, если нужная высота будет выставлена по умолчанию)
            ctable.set_row_min_hight(i_row, table_rows_hight)
             # Установить новый текст
            ctable.set_row_text(i_row, row_text)
            ctable.set_node_property(column_cmd_rx, i_row, 'pressed', cls.rxe)
            ctable.set_node_property(column_cmd_tx, i_row, 'pressed', cls.txe)


## Находит пересечения диапазонов
func find_inters(info: Dictionary) -> Array:
    # info - словарь, его структура
    # {
    #   "устройство": # имя устройства ("эмс-ор63")
    #   [
    #     {
    #       cname,      # имя кластера ("Кластер-0")
    #       wfb,        # ширина диапазона, МГЦ (800.0)
    #       cfb,        # центр диапазона, МГЦ (2000.0)
    #       txe,        # передатчик включен
    #       rxe         # передатчик выключен
    #     },
    #     {
    #       cname,      # имя кластера ("Кластер-1")
    #       wfb,        # ширина диапазона, МГЦ (200.0)
    #       cfb,        # центр диапазона, МГЦ (1000.0)
    #       txe,        # передатчик включен
    #       rxe         # передатчик выключен
    #     },
    #   ]
    # }
    for nm in info:
        # nm - имя устройства
        var unit_data = info[nm]
        if not unit_data: continue
        for cls in unit_data.mcls:
            # cls - один кластер
            #print_debug([cls.cname, cls.wfb, cls.cfb, cls.txe, cls.rxe])
            pass
    # заглушка для возвращаемых данных
    return [['эмс-ор63', 'эмс-ор63', 'Кластер-0', 'Кластер-0', 1600.0, 2400.0]]


## Отображает кластеры на шкале
## [param nm] - имя устройства
func map_yems_data_to_scale(unit_data: Dictionary, claster_scale: GridContainer):
    # Обойти все кластеры устройства
    for cls in unit_data.mcls:
        # Преобразовать диапазоны частот в диапазоны отображения
        var c: = remap(cls.cfb, freq_min, freq_max, 0.0, 1.0)
        var w: = remap(cls.wfb, freq_min, freq_max, 0.0, 0.5)
        # Чтобы диапазон всегда был виден, даже если он слишком узкий
        w = clampf(w, 0.005, 1.0)
        # Отсечь значения по границам
        var l: = clampf(c - w, 0.0, 1.0)
        var h: = clampf(c + w, 0.0, 1.0)
        # Получить шкалу приёмника
        var scale_rx: = claster_scale.get_node('panel_rx')
        # Отобразить диапазон приёмника, в соответствии с разрешением
        var color_rxe: = color_enabled if cls.get('rxe', false) else color_disabled
        scale_rx.set_range(cls.cname, l, h, color_rxe)
        # Получить шкалу передатчика
        var scale_tx: = claster_scale.get_node('panel_tx')
        # Отобразить диапазон передатчика, в соответсвии с разрешением
        var color_txe: = color_enabled if cls.get('txe', false) else color_disabled
        scale_tx.set_range(cls.cname, l, h, color_txe)
