extends GridContainer

## Виджет "Таблица"

@export var node_gap:                =  Vector2(0.0, 0.0)    ## Зазор между ячейками, который не возможно вычислить или получить
@export var selector_color:          =  Color(1.0, 1.0, 1.0, 1.0)  ## Окрашивающий цвет для индикатора выбранного ряда
@export var selector_key_down: Key   =  KEY_DOWN  ## Клавиша увеличения индекса выбранного ряда
@export var selector_key_up: Key     =  KEY_UP    ## Клавиша уменьшения индекса выбранного ряда
@export_file var node_type_file:     = 'res://table/node.tscn': get = get_node_scene, set = load_node_scene  ## Сцена ячейки устанавливаемая по умолчанию
@export_file var selector_type_file: = 'res://table/selector.tscn': set = load_selector_scene  ## Сцена индикатора для выбранного ряда

const HEAD_PREFIX  = 'HEAD_'      ## Префикс ключа заголовка таблицы
const INDEX_FORMAT = '%02d_%02d'  ## Формат индекса ключа
const MAX_INDEX    = 99           ## Максимальное значение индекса для колонки и строки. Связано с форматом индекса ключа
const NODE_PREFIX  = 'NODE_'      ## Префикс ключа ячейки таблицы
const SLCT_PREFIX  = 'SLCT_'      ## Префикс ключа индикатора выбранного ряда таблицы

const HEAD_KEY_FORMAT         = HEAD_PREFIX + INDEX_FORMAT  ## Формат ключа заголовка
const NODE_KEY_FORMAT         = NODE_PREFIX + INDEX_FORMAT  ## Формат ключа ячейки
const ROW_SELECTOR_KEY_FORMAT = SLCT_PREFIX + INDEX_FORMAT  ## Формат ключа индикаторов выбранного ряда
const META_NAME_USER_DATA     = 'user_data'
const META_NAME_CALLEBLE      = 'calleble'

var header: GridContainer          ## Заголовок таблицы
var in_focus: = false              ## Фокус пользовательского ввода
var nodes: = {}                    ## Все ячейки таблицы и дополнительные элементы
var ScnNode: PackedScene           ## Прототип ячейки таблицы
var ScnSelector: PackedScene       ## Прототип индикатора выбранного ряда
var selector_color_proc: Callable  ## Ссылка на процедуру генератора цвета индикатора выбранного ряда
var selector_size_proc:  Callable  ## Ссылка на процедуру генератора размера индикатора выбранного ряда

signal row_pressed(i_row: int)       ## Вызывается при нажатии кнопкой мыши на строку
signal selected_changed(i_row: int)  ## Вызывается при изменении выбранного ряда. i_row - текущий выбранный ряд

var scroll: Control
var scroll_adj: = Vector2(0, 0)


func get_node_scene(): return node_type_file
func _assert_meta(i_col: int, i_row: int, pname: String, node): assert(node.has_meta(pname), 'ошибка: нет мета-свойства \"%s\" у ячейки колонка:%d ряд:%d' % [pname, i_col, i_row])
func get_node_user_data(i_col: int, i_row: int): return get_node_meta(i_col, i_row, META_NAME_USER_DATA)
func set_node_user_data(i_col: int, i_row: int, val) -> void: set_node_meta(i_col, i_row, META_NAME_USER_DATA, val)


## Закрепляет заголовок таблицы. [param scrl] - Прокрутка,
## [param prnt] - Родитель прокрутки заданный в редакторе
func pin_header(scrl: ScrollContainer, prnt: Control):
    scrl.remove_child(header)
    prnt.add_child(header)
    ## TODO: Это значение следует получить из темы
    var v_separation: = 3.0
    header.position = scrl.position - Vector2(0, header.size.y + v_separation)
    scroll = scrl
    scroll_adj = scrl.position


## Перемещает указатель на одну строку вверх
func selector_up():
    var i_rows: = []
    get_selected_rows(i_rows)
    for i in i_rows:
        set_row_selected(i, false)
    var rows_count: = get_rows_count()
    if not (i_rows.size() and (rows_count > 1)):
        return
    var i_row = i_rows[0]
    i_row -= 1
    if i_row < 0:
        i_row = rows_count - 1
    set_row_selected(i_row, true)
    emit_signal('selected_changed', i_row)


## Перемещает указатель на одну строку вниз
func selector_down():
    var i_rows: = []
    get_selected_rows(i_rows)
    for i in i_rows:
        set_row_selected(i, false)
    var rows_count: = get_rows_count()
    if not (i_rows.size() and (rows_count > 1)):
        return
    var i_row = i_rows[0]
    i_row += 1
    i_row %= rows_count
    set_row_selected(i_row, true)
    emit_signal('selected_changed', i_row)


##
func set_columns_alignments(alignments):
    assert(columns == alignments.size())
    var rows_count: = get_rows_count()
    for i_row in rows_count:
        for i_col in alignments.size():
            set_node_alligment(i_col, i_row, alignments[i_col])


##
func load_selector_scene(file_name: String):
    selector_type_file = file_name
    ScnSelector = load(file_name)


##
func load_node_scene(file_name: String):
    node_type_file = file_name
    ScnNode = load(file_name)


##
func _ready():
    ScnSelector = load(selector_type_file)
    ScnNode = load(node_type_file)
    assert(ScnSelector)
    assert(ScnNode)


## Выравнивает по нижнему левому углу. Используется для выравнивания по заголовку
func align_to_left_bottompos(hdr: Control, gap: = Vector2(0, 0)):
    position.x = hdr.position.x + gap.x
    position.y = hdr.position.y + hdr.size.y + gap.y


## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
func set_node_meta(i_col: int, i_row: int, pname: String, val) -> void:
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    var node = nodes[key]
    node.set_meta(pname, val)


## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
func get_node_meta(i_col: int, i_row: int, pname: String):
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    var node = nodes[key]
    _assert_meta(i_col, i_row, pname, node)
    return node.get_meta(pname)


## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
func set_node_property(i_col: int, i_row: int, pname: String, val) -> void:
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    var node = nodes[key]
    node.set(pname, val)


## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
func get_node_property(i_col: int, i_row: int, pname: String):
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    var node = nodes[key]
    return node.get(pname)


## [param i_col] - индекс колонки. [param i_row] - индекс ряда
func get_node_key(format: String, i_col: int, i_row: int) -> String:
    var key: String = format % [i_row, i_col]
    assert(nodes.has(key), 'ошибка: нет такой ячейки - колонка:%d ряд:%d' % [i_col, i_row])
    return key


##
func get_key_indexes(key: String) -> Array:
    var tokens: = key.split('_')
    assert(tokens.size() == 3, 'ошибка: неверный ключ: \"%s\"' % key)
    return [int(tokens[2]), int(tokens[1])]


##
func set_columns_min_size(sizes: Array) -> void:
    var rows_count: = get_rows_count()
    for i_row in rows_count:
        set_nodes_min_size(i_row, sizes)
    set_nodes_min_size(0, sizes, HEAD_KEY_FORMAT)


##
func set_columns_aligment(alignments: Array) -> void:
    var rows_count: = get_rows_count()
    for i_row in rows_count:
        for i_col in alignments.size():
            set_node_alligment(i_col, i_row, alignments[i_col])


##
func _input(event) -> void:
    if event is InputEventKey:
        if in_focus and event.pressed:
            if event.keycode == selector_key_up:
                selector_up()
            elif event.keycode == selector_key_down:
                selector_down()
    elif event is InputEventMouseButton:
        if not event.pressed:
            return
        if check_event_pos(event.position, scroll):
            if not in_focus:
                in_focus = true
                emit_signal('focus_entered')
        elif not check_event_pos(event.position, scroll):
            if in_focus:
                in_focus = false
                emit_signal('focus_exited')


##
func _init() -> void:
    selector_color_proc = Callable(self, 'defaultselector_color_proc')
    assert(selector_color_proc.is_valid())
    selector_size_proc = Callable(self, 'default_selector_size_proc')
    assert(selector_size_proc.is_valid())
    scroll = self


##
func check_event_pos(event_pos: Vector2, obj: Control):
    return                                              \
        (event_pos.x >  obj.position.x)                 \
    and (event_pos.x < (obj.position.x + obj.size.x))   \
    and (event_pos.y >  obj.position.y)                 \
    and (event_pos.y < (obj.position.y + obj.size.y))


##
func defaultselector_color_proc(_i_row: int):
    return selector_color


##
func setselector_color_proc(instance: Object, function: String) -> void:
    selector_color_proc = Callable(instance, function)
    assert(selector_color_proc.is_valid())


##
func set_selector_size_proc(instance: Object, function: String) -> void:
    selector_size_proc = Callable(instance, function)
    assert(selector_size_proc.is_valid())


## [param i_row] - Индекс ряда
func default_selector_size_proc(i_row: int, rect: Array) -> void:
    get_row_rect(i_row, rect)
    rect[0] += position.x - node_gap.x / 2.0
    rect[1] += position.y - node_gap.y / 2.0
    rect[2] += node_gap.x
    rect[3] += node_gap.y


##
func clear_rows_selected():
    var rows_count: = get_rows_count()
    for i_row in rows_count:
        set_row_selected(i_row, false)


## [param i_row] - Индекс ряда.
func set_row_selected(i_row: int, selected: bool = true) -> void:
    var key: = ROW_SELECTOR_KEY_FORMAT % [i_row, 0]
    var node = nodes[NODE_KEY_FORMAT % [i_row, 0]]
    if nodes.has(key) and (not selected):
        var selector = nodes[key]
        node.remove_child(selector)
        selector.queue_free()
        nodes.erase(key)
    elif (not nodes.has(key)) and selected:
        var selector = ScnSelector.instantiate()
        var rect: = [0, 0, 0, 0]
        selector_size_proc.call(i_row, rect)
        selector.size.x = rect[2]
        selector.size.y = rect[3]
        selector.modulate = selector_color_proc.call(i_row)
        selector.z_index = z_index + 1
        nodes[key] = selector
        node.add_child(selector)


## [param i_row] - Индекс ряда.
func is_row_selected(i_row: int) -> bool:
    var key: = ROW_SELECTOR_KEY_FORMAT % [i_row, 0]
    return nodes.has(key)


## [param i_row] - Индексы рядов.
func get_selected_rows(i_rows: Array) -> void:
    for key in nodes.keys():
        if SLCT_PREFIX in key:
            var indexes = get_key_indexes(key)
            i_rows.append(indexes[1])


## Возвращает количество рядов
func get_rows_count() -> int:
    var nodes_count: int = 0
    for key in nodes:
        nodes_count += int(NODE_PREFIX in key)
    return nodes_count / columns


##
func get_node2(i_col: int, i_row: int) -> Node:
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    return nodes[key]


##
func add_row(row_types: Array) -> void:
    assert(row_types.size() < MAX_INDEX, 'ошибка: слишком много колонок, больше %d' % (MAX_INDEX + 1))
    if columns == 1:
        set_columns(row_types.size())
    else:
        assert(columns == row_types.size(), 'ошибка: %s количество колонок установлено ранее - %d, но требуется %d' % [self, columns, row_types.size()])
    var i_row: int = get_rows_count()
    assert(i_row < MAX_INDEX, 'ошибка: слишком много рядов, больше %d' % (MAX_INDEX + 1))
    for i_col in columns:
        var node_type = row_types[i_col]
        var node = node_type.instantiate()
        assert(node.name == node.name, 'ошибка: экземпляр не имеет поля \"name\"')
        var head = nodes[HEAD_KEY_FORMAT % [0, i_col]]
        node.custom_minimum_size.x = head.custom_minimum_size.x
        node.set_deferred('size.x', head.size.x)
        var node_key: String = NODE_KEY_FORMAT % [i_row, i_col]
        node.name = node_key
        add_child(node)
        nodes[node_key] = node
        var conn = Callable(self,  '_on_focus_entered').bind(node)
        node.connect('focus_entered', conn)
        set_node_meta(i_col, i_row, 'callable_focus_entered', conn)


##
func _on_focus_entered(node):
    var indexes = get_key_indexes(node.name)
    emit_signal('row_pressed', indexes[1])


##
func set_header(header_types: Array):
    assert(header_types.size() < MAX_INDEX, 'ошибка: количество колонок в заголовке: %d больше допустимого: %d' % [header_types.size(), MAX_INDEX + 1])
    if columns == 1:
        set_columns(header_types.size())
    else:
        assert(columns == header_types.size(), 'ошибка: количество колонок в заголовке %d, но ранее установлено: %d' % [header_types.size(), columns])
    var do_adj: = false
    if not header:
        header = GridContainer.new()
        header.position = position
        header.columns = header_types.size()
        header.size.x = size.x
        var parent = scroll.get_parent()
        parent.add_child(header)
        do_adj = true
    else:
        var childrens = header.get_children()
        for node in childrens:
            childrens.erase(node.name)
            childrens.remove_child(node)
            node.queue_free()
    for i_col in columns:
        var node = header_types[i_col].instantiate()
        var key: String = HEAD_KEY_FORMAT % [0, i_col]
        node.name = key
        nodes[key] = node
        header.add_child(node)
    if do_adj:
        var rect = [0, 0, 0, 0]
        get_node_rect(0, 0, rect, HEAD_KEY_FORMAT)
        position.y += rect[3]


##
func set_header_text(headers: Array):
    assert(columns == headers.size())
    set_row_text(0, headers, 0, HEAD_KEY_FORMAT)


##
func remove_row(i_row: int):
    var sel_key: String = ROW_SELECTOR_KEY_FORMAT % [i_row, 0]

    if nodes.has(sel_key):
        var selector = nodes[sel_key]
        var prnt = selector.get_parent()
        prnt.remove_child(selector)
        selector.queue_free()
        nodes.erase(sel_key)

    for i_col in columns:

        var node_key: String = NODE_KEY_FORMAT % [i_row, i_col]
        var node = nodes[node_key]

        var conn = get_node_meta(i_col, i_row, 'callable_focus_entered')
        node.disconnect('focus_entered', conn)

        if node.has_meta(META_NAME_CALLEBLE):
            conn = node.get_meta(META_NAME_CALLEBLE)
            node.disconnect('pressed', conn)

        remove_child(node)
        nodes.erase(node_key)
        node.queue_free()
    rehash()


##
func next_index(indexes: Array) -> void:
    indexes[0].x += 1
    if indexes[0].x == columns:
        indexes[0].x = 0
        indexes[0].y += 1


##
func swap_nodes(nodes_copy, node_key_format, i0, i1) -> void:
    var key0 = node_key_format % [i0.y, i0.x]
    var key1 = node_key_format % [i1.y, i1.x]
    nodes_copy[key1] = nodes[key0]
    nodes_copy[key1].name = key1


## Переназначает ключи в соответствии с размерами таблицы.
func rehash():
    var i_node: = [Vector2i()]
    var i_hdr:  = [Vector2i()]
    var i_slct: = [Vector2i()]
    var nodes_copy = {}
    for key in nodes:
        var iv = get_key_indexes(key)
        var i0 = Vector2(iv[0], iv[1])
        if  HEAD_PREFIX in key:
            swap_nodes(nodes_copy, HEAD_KEY_FORMAT,         i0, i_hdr[0])
            next_index(i_hdr)
        elif NODE_PREFIX in key:
            swap_nodes(nodes_copy, NODE_KEY_FORMAT,         i0, i_node[0])
            next_index(i_node)
        elif SLCT_PREFIX in key:
            swap_nodes(nodes_copy, ROW_SELECTOR_KEY_FORMAT, i0, i_slct[0])
            next_index(i_slct)
    nodes.clear()
    nodes.merge(nodes_copy)


## Удаляет все строки таблицы
func clear_rows() -> void:
    var rows_count: int = get_rows_count()
    for i_row in range(rows_count - 1, -1, -1):
        remove_row(i_row)


## Настраевает обратные вызовы для колонок.
## [param dest_node] - Получатель события[br]
## [param event_name] - Имя события[br]
## [param callback] - Имя функции получателя события[br]
## [param clmns] - Индексы колонок[br]
## [param user_data] - Аргументы функции получателя события[br]
func connect_columns(dest_node: Node, event_name: String, callback: String, clmns: Array, user_data: Array) -> void:
    var rows_count = get_rows_count()
    for i_row in rows_count:
        for i_col in clmns:
            assert(i_col >= 0 and i_col < columns, 'ошибка: нет такой колонки: %d' % i_col)
            var node = get_node2(i_col, i_row)
            if node.has_meta(META_NAME_CALLEBLE):
                var conn = node.get_meta(META_NAME_CALLEBLE)
                node.disconnect(event_name, conn)
            var conn = Callable(dest_node, callback).bind([node] + user_data)
            assert (OK == node.connect(event_name, conn))
            node.set_meta(META_NAME_CALLEBLE, conn)

##
func set_rows_text(rows: Array) -> void:
    for i_row in rows.size():
        for i_col in columns:
            var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
            var node = nodes[key]
            node.text = rows[i_row][i_col]


##
func set_nodes_min_size(i_row: int, sizes: Array, key_format: String = NODE_KEY_FORMAT) -> void:
    for i_col in columns:
        var key: = get_node_key(key_format, i_col, i_row)
        var node = nodes[key]
        node.custom_minimum_size.x = sizes[i_col]
        node.set_deferred('size.x', sizes[i_col])


##
func set_node_text(i_col: int, i_row: int, val: String) -> void:
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    nodes[key].text = val


##
func set_rows_state(rows: Array) -> void:
    for i_row in rows.size():
        for i_col in columns:
            var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
            nodes[key].button_pressed = rows[i_row][i_col]


##
func set_node_state(i_col: int, i_row: int, val: bool) -> void:
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    nodes[key].button_pressed = val


## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
func set_node_frame(i_col: int, i_row: int, node_name: String, val: int) -> void:
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    var node = nodes[key]
    var sprite = node.get_node(node_name)
    sprite.frame = val


## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
func set_node_editable(i_col: int, i_row: int, val: bool) -> void:
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    nodes[key].editable = val


## [param i_col] - индекс колонки. [param i_row] - индекс ряда.
func set_node_alligment(i_col: int, i_row: int, val: int) -> void:
    var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
    if 'horizontal_alignment' in nodes[key]:
        nodes[key].horizontal_alignment = val
    elif 'alignment' in nodes[key]:
        nodes[key].alignment = val


## [param i_row] - индекс ряда.
func set_row_alligments(i_row: int, aligns: Array) -> void:
    for i_col in columns:
        var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
        nodes[key].align = aligns[i_col]


## [param i_row] - индекс ряда.
func get_row_text(i_row: int) -> Array:
    var row: = []
    for i_col in columns:
        var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
        row.append(nodes[key].text)
    return row


##
func set_row_text(i_row: int, row: Array, i_begin: int = 0, key_format: String = NODE_KEY_FORMAT) -> void:
    assert(i_begin >= 0)
    assert((columns - i_begin) <= row.size())
    for i_col in columns - i_begin:
        var key: = get_node_key(key_format, i_col + i_begin, i_row)
        nodes[key].text = row[i_col]


## Возвращает все строки таблицы в двумерном массиве
func get_rows_text() -> Array:
    var rows_count: = get_rows_count()
    var rows: = []
    for i_row in rows_count:
        var row = get_row_text(i_row)
        rows.append(row)
    return rows


## Возвращает массив ячеек из ряда [param i_row]
func get_row(i_row: int) -> Array:
    var row: = []
    for i_col in columns:
        var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row)
        row.append(nodes[key])
    return row


## Возвращает прямоугольник [param rect] занимаемый ячейкой в ряду [param i_row] и колонке [param i_col].
## [param key_format] - Формат ключа ячейки
func get_node_rect(i_col: int, i_row: int, rect: Array, key_format: String = NODE_KEY_FORMAT) -> void:
    assert(rect.size() >= 4, 'ошибка: мало элементов для возврата результата')
    var key: = get_node_key(key_format, i_col, i_row)
    var cell = nodes[key]
    rect[0] = cell.position.x
    rect[1] = cell.position.y
    rect[2] = cell.size.x
    rect[3] = cell.size.y


## Возвращает прямоугольник [param rect] занимаемый рядом [param i_row]. [param key_format] - Формат ключа ячейки
func get_row_rect(i_row: int, rect: Array, key_format: String = NODE_KEY_FORMAT) -> void:
    assert(rect.size() >= 4, 'ошибка: мало элементов для возврата результата')
    var cell0 = nodes[get_node_key(key_format, 0, i_row)]
    var cell1 = nodes[get_node_key(key_format, columns - 1, i_row)]
    rect[0] = cell0.position.x + scroll_adj.x
    rect[1] = cell0.position.y + scroll_adj.y
    rect[2] = (cell1.position.x - cell0.position.x) + cell1.size.x
    rect[3] = cell0.size.y
