@tool
class_name Network extends Node

const YAU07_PROTO: = {'yau07tx': yau07.YaU07}
const UDP_PROTO: = {'capsrpb': capsrpb.CapsRpb, 'trassa': trassa.Trassa}
const TCP_PROTO: = {'5p28': tcp5p28.TCP5P28}
const SERIAL_PROTO: = {'spt25': spt25.SPT25}
const MODBUS_PROTO: = {'mbcs': sch3.Sch3}
const NAVI_PROTO: = {'navi': navi.NaviSource}
const NULL_PROTO: = {null: unit.Unit}
var tick: int = 0


class SocketSerial extends SerialPort:
    const P_DIV: = 0x8408
    const FLAG: = 0x7e
    const FLAG_MASK: = 0x20
    const FSC_MASK: = 0x8000
    const parity_code = {
        'N': SerialPort.PARITY_NONE,
        'E': SerialPort.PARITY_EVEN,
        'O': SerialPort.PARITY_ODD }
    static var tick: int
    var unit_instance: unit.Unit
    var failed: = {}

    func _init():
        connect('got_error', on_serial_got_error)

    func on_serial_got_error(where: String, what: String):
        if where not in failed:
            push_error([where, what, unit_instance])
            failed[where] = what

    func on_serial_data(data: PackedByteArray):
        failed.clear()
        unit_instance.parse(data, tick)

    func format_buffer(data: PackedByteArray):
        var buffer: PackedByteArray
        buffer.resize(64)
        var i: = 1
        for v in data:
            if v != 0x7e:
                buffer[i] = v; i += 1
            else:
                buffer[i] = FLAG; i += 1
                buffer[i] = FLAG ^ FLAG_MASK; i += 1
        var v_sum: = get_check_sum(data)
        buffer.encode_u16(i, v_sum); i += 2
        buffer[i] = FLAG; i += 1
        buffer.resize(i)
        return buffer

    func send_to(_data: PackedByteArray):
        if not is_open():
            call_deferred('try_open')
            return
        # var buffer = format_buffer(data)  # Не проверено, но должно работать
        # Эта последовательность для запроса состояния, получена от разработчиков СПТ-25
        const tx_tula: = [0x7e, 0x01, 0x04, 0xbb, 0x50, 0x7e]
        var sz = write_raw(tx_tula)
        if sz != tx_tula.size():
            call_deferred('try_close')

    func _to_string() -> String: return 'последовательный порт \"%s\" @ %d (%s)' % [self.port, self.baudrate, 'открыт' if is_open() else 'закрыт']

    func get_check_sum(src: PackedByteArray) -> int:
        var fcs: = (~((src[1] << 8) + src[0])) & 0xffff
        for i: int in range(2, src.size()):
            var chr: = src[i]
            for j: int in 8:
                if chr & 1:
                    if fcs & 1:
                        fcs = ((fcs >> 1) | FSC_MASK) ^ P_DIV
                    else:
                        fcs = ((fcs >> 1) | FSC_MASK)
                else:
                    fcs = (fcs >> 1) ^ P_DIV if fcs & 1 else fcs >> 1
                chr >>= 1
        for j: int in 16:
            fcs = (fcs >> 1) ^ P_DIV if fcs & 1 else fcs >> 1
        return (~fcs) & 0xffff

    func try_open() -> Error:
        if is_open():
            return Error.OK
        var rc: = open(port)
        if rc == Error.OK:
            data_received.connect(on_serial_data)
            start_monitoring(timeout)
        return rc

    func try_close():
        if not is_open():
            return Error.FAILED
        stop_monitoring()
        close()
        var connections: = data_received.get_connections()
        for conn in connections:
            data_received.disconnect(conn.callable)
        return Error.OK


class SocketUDP extends PacketPeerUDP:
    func send_to(addr: Array, data: PackedByteArray):
        set_dest_address(addr[0], addr[1])
        var rc: = put_packet(data)
        if rc != Error.OK:
            push_error('%s: %s', [error_string(rc), addr])


class SocketTCP extends TCPServer:
    const RX_TIMEOUT: int = 1000    ## Время сброса счётчика принятия части пакета.
    var peerstream: = PacketPeerStream.new()
    var unit_name: StringName       ## Уникальное имя устройства
    var head_len: int = 8           ## Длина заголовка команды.
    var len_place: int = 4          ## Номер начального байта с длинной блока данных.
    var type_len: int = 4           ## Количество байт в размере длинны.
    var rx_tick: int                ## Время приёма последнего пакета.
    var rx_all: bool = true         ## Флаг, что пакет принят полностью.
    var rx_len: int                 ## Количество принятых байт.
    var rx_data: PackedByteArray    ## Принятые данные.
    var data_len: int               ## Размер ожидаемых данных.

    func send_to(data: PackedByteArray):
        var peer = peerstream.get_stream_peer()
        peer.put_data(data)


var poll_sockets: Array[SocketUDP]                     ## Сокеты для непрерывного опроса наличия новых данных
var tcp_sockets: Array[Array]                          ## Сокеты для TCP-соединений
var units: Dictionary[StringName, unit.Unit]           ## Экземпляры всех сетевых устройств
var units_udptx: Dictionary[StringName, unit.Unit]     ## Экземпляры устройств с udp-соединением которым нужно передавать данные
var units_udprx: Dictionary[StringName, unit.Unit]     ## Экземпляры устройств с udp-соединением от которых приходят данные
var units_tcp: Dictionary[StringName, unit.Unit]       ## Экземпляры устройств с tcp-соединением
var units_serial: Dictionary[StringName, unit.Unit]    ## Экземпляры устройств с uart-соединением
var units_modbus: Dictionary[StringName, unit.Unit]    ## Экземпляры устройств с modbusserial-соединением
var units_nullp: Dictionary[StringName, unit.Unit]     ## Экземпляры устройств без протокола
var serials: Dictionary[StringName, SocketSerial]      ## Последовательные порты (uart)
var dst_ports: Dictionary[StringName, int]             ## Преобразование имя устройства в порт назначения
var dst_addrs: Dictionary[StringName, StringName]      ## Преобразование имя устройства в адрес назначения
var port_to_unit_name: Dictionary[int, StringName]     ## Преобразование порта назначения в имя устройства
var logger_page: Node                                  ## Ссылка на журнал работы
var send_sockets: Dictionary[StringName, SocketUDP]    ## Таблица <имяустройста>: <сокет> для отправки сообщений
var addr_port_to_unit_name: Dictionary[StringName, StringName]  ## Преобразование адрес:порт в имя устройства


func on_settings_changed(serial: SocketSerial):
    serial.try_close()
    var rc = serial.try_open()
    if rc != Error.OK:
        log.message(log.ERROR, 'невозможно открыть %s для \"%s\"' % [serial.port, serial.unit_instance.name])
    else:
        log.message(log.INFO, 'открыт %s для \"%s\"' % [serial.port, serial.unit_instance.name])


## [param unit_name] - Уникальное имя устройства[br]
func create_socket_udp(unit_name: StringName) -> SocketUDP:
    var st = settings.UnitProfiles[unit_name][1]
    var addr = st[0]
    var port = st[1]
    var broad = st[2]
    var bind = st[3]
    var sock: = SocketUDP.new()
    sock.set_broadcast_enabled(broad)
    var rc: = sock.bind(port, addr)
    if rc == Error.OK:
        log.info('\"%s\" привязан к %s:%d' % [unit_name, addr, port])
    else:
        var errmsg: = error_string(rc)
        log.error('привязка \"%s\" к %s:%d не удалась: \"%s\"' % [unit_name, addr, port, errmsg])
    return sock


func create_socket_caps(unit_name: StringName) -> SocketUDP:
    var st = settings.UnitProfiles[unit_name][1]
    var addr = st[1]
    var port = st[2]
    var sock: = SocketUDP.new()
    var rc: = sock.bind(port, addr)
    if rc == Error.OK:
        log.info('\"%s\" привязан к %s:%d' % [unit_name, addr, port])
    else:
        var errmsg = error_string(rc)
        log.error('привязка \"%s\" к %s:%d не удалась: \"%s\"' % [unit_name, addr, port, errmsg])
    return sock


## [param unit_name] - Уникальное имя устройства[br]
func create_socket_tcp(unit_name: StringName) -> SocketTCP:
    var st = settings.UnitProfiles[unit_name][1]
    var addr  = st[0]
    var port  = st[1]
    var sock: = SocketTCP.new()
    sock.unit_name = unit_name
    var rc: = sock.listen(port, addr)
    if rc == Error.OK:
        log.message(log.INFO, '\"%s\" слушает на %s:%d' % [unit_name, addr, port])
    else:
        log.message(log.ERROR, '\"%s\" %s:%d \"%s\"' % [unit_name, addr, port, error_string(rc)])
    return sock


## [param unit_name] - Уникальное имя устройства[br]
func create_serial(unit_instance: unit.Unit) -> SocketSerial:
    var st = settings.UnitProfiles[unit_instance.name][1]
    var serial: = SocketSerial.new()
    serial.port = st[0]
    serial.baudrate = st[2]
    serial.parity = SocketSerial.parity_code.get(st[3], SocketSerial.PARITY_NONE)
    serial.timeout = 20000
    serial.bytesize = st[4]
    serial.stopbits = st[5]
    serial.unit_instance = unit_instance
    ProjectSettings.connect('settings_changed', func(): on_settings_changed(serial))
    return serial


## [param unit_name] - Уникальное имя устройства[br]
func create_modbus(unit_name: StringName) -> sch_3.Sch3:
    var unit_modbus: = sch_3.Sch3.new(unit_name)
    return unit_modbus


func _ready() -> void:
    if Engine.is_editor_hint(): return
    var sock_unicast: = create_socket_udp('уарэп-яу07-частный')
    var sock_broadcast: = create_socket_udp('уарэп-яу07-общий')
    var sock_navi: = create_socket_udp('навигация')
    var sock_caps: = create_socket_caps('уарэп-капсрпб')
    var sock_trassa: = create_socket_caps('уарэп-трасса')
    poll_sockets = [sock_unicast, sock_broadcast, sock_caps, sock_navi, sock_trassa]
    send_sockets['уарэп-яу07-частный'] = sock_unicast
    send_sockets['уарэп-яу07-общий'] = sock_broadcast
    send_sockets['уарэп-капсрпб'] = sock_caps
    send_sockets['уарэп-трасса'] = sock_trassa
    send_sockets['уарэп-эмс'] = sock_unicast
    send_sockets['уарэп-яу07-2в'] = sock_unicast
    send_sockets['уарэп-яу07-2н'] = sock_unicast
    send_sockets['уарэп-яу07-2к'] = sock_unicast
    send_sockets['уарэп-яу07-4в'] = sock_unicast
    send_sockets['уарэп-яу07-4н'] = sock_unicast
    send_sockets['уарэп-яу07-4к'] = sock_unicast
    send_sockets['уарэп-яу07-1в'] = sock_unicast
    send_sockets['уарэп-яу07-1н'] = sock_unicast
    send_sockets['уарэп-яу07-1к'] = sock_unicast
    send_sockets['уарэп-яу07-3в'] = sock_unicast
    send_sockets['уарэп-яу07-3н'] = sock_unicast
    send_sockets['уарэп-яу07-3к'] = sock_unicast

    var bind_serials: = []
    var bind_modbuses: = []

    for unit_name in settings.UnitProfiles:
        var unit_profile = settings.UnitProfiles[unit_name][1]
        var proto = settings.UnitProfiles[unit_name][0]

        if proto in NAVI_PROTO:
            var new_unit = NAVI_PROTO[proto].new(unit_name)
            units[unit_name] = new_unit
            units_udprx[unit_name] = new_unit
            dst_ports[unit_name] = unit_profile[1]

        if proto in YAU07_PROTO:
            var new_unit = YAU07_PROTO[proto].new(unit_name)
            units[unit_name] = new_unit
            units_udptx[unit_name] = new_unit
            dst_addrs[unit_name] = unit_profile[0]
            dst_ports[unit_name] = unit_profile[1]
            var addr_port: = StringName('%s:%d' % [unit_profile[0], unit_profile[1]])
            addr_port_to_unit_name[addr_port] = unit_name

        if proto in UDP_PROTO:
            var new_unit = UDP_PROTO[proto].new(unit_name)
            units[unit_name] = new_unit
            units_udptx[unit_name] = new_unit
            dst_addrs[unit_name] = unit_profile[0]
            dst_ports[unit_name] = unit_profile[3]

        if proto in TCP_PROTO:
            var unit_tcp = TCP_PROTO[proto].new(unit_name)
            units[unit_name] = unit_tcp
            units_tcp[unit_name] = unit_tcp

        if proto in MODBUS_PROTO:
            var unit_modbus = MODBUS_PROTO[proto].new(unit_name)
            unit_modbus.port = unit_profile[0]
            unit_modbus.params = unit_profile.slice(1)
            units[unit_name] = unit_modbus
            units_modbus[unit_name] = unit_modbus
            bind_modbuses.append([unit_modbus, unit_name])

        if proto in SERIAL_PROTO:
            var unit_instance = SERIAL_PROTO[proto].new(unit_name)
            var serial: = create_serial(unit_instance)
            units_serial[unit_name] = unit_instance
            units[unit_name] = unit_instance
            serials[unit_name] = serial
            bind_serials.append([serial, unit_instance, unit_name])

        if proto in NULL_PROTO:
            var unit_null = NULL_PROTO[proto].new(unit_name)
            units[unit_name] = unit_null
            units_nullp[unit_name] = unit_null


    tcp_sockets.append([create_socket_tcp('уарэп-5п28'), units_tcp['уарэп-5п28']])
    for key: StringName in dst_ports:
        port_to_unit_name[dst_ports[key]] = key
    if logger_page:
        for key in units:
            var u = units[key]
            u.connect('line_changed', Callable(logger_page, 'on_line_changed'))
            u.connect('command_fail', Callable(logger_page, 'on_command_fail'))


func _on_settings_changed_modbus(bind_modbuses: Array):
    var _external_ctl = ProjectSettings.get_setting('application/config/Внешнее управление', false)
    for bm in bind_modbuses:
        var _unit_modbus = bm[0]
        var _unit_name = bm[1]


func poll_receive_udp(sock: SocketUDP) -> bool:
    while sock.get_available_packet_count() > 0:
        var data: = sock.get_packet()
        var addr: = sock.get_packet_ip()
        var port: = sock.get_packet_port()
        var addr_port: = StringName('%s:%d' % [addr, port])
        if addr_port_to_unit_name.has(addr_port):
            var unit_name: = addr_port_to_unit_name[addr_port]
            units[unit_name].parse(data, tick)
            continue
        if port_to_unit_name.has(port):
            var unit_name: = port_to_unit_name[port]
            if units.has(unit_name):
                units[unit_name].parse(data, tick)
            continue
        var local_port: = sock.get_local_port()
        if port_to_unit_name.has(local_port):
            var unit_name: = port_to_unit_name[local_port]
            var unit_udprx: = units[unit_name]
            unit_udprx.source_addr = [addr, port]
            unit_udprx.parse(data, tick)
            continue
    return false


func poll_receive_tcp(sock: SocketTCP) -> bool:
    if sock.is_connection_available(): # Проверить, если кто то пытается подключиться
        var client: = sock.take_connection() # Принять соединение
        sock.peerstream.set_stream_peer(client) # Привязать поток к новому клиенту
        log.info('подключен: %s:%d к порту: %d' % [ client.get_connected_host(), client.get_connected_port(), client.get_local_port() ])
    var peer = sock.peerstream.get_stream_peer()
    if peer:
        while true:
            var peer_len_data: = peer.get_available_bytes()
            if peer_len_data <= 0:
                break
            get_client_data(sock, peer, peer_len_data)
            if sock.unit_name in units_tcp and sock.rx_all:
                units_tcp[sock.unit_name].parse(sock.rx_data, tick)
    return false


func get_client_data(sock: SocketTCP, peer: StreamPeer, peer_len_data: int) -> void:
    if tick - sock.rx_tick > sock.RX_TIMEOUT:
        sock.rx_all = true
    sock.rx_tick = tick
    if sock.rx_all:
        if peer_len_data >= sock.head_len:
            sock.rx_all = false
            sock.rx_len = sock.head_len
            var head = peer.get_data(sock.head_len)
            sock.data_len = head[1].decode_u32(sock.len_place)
            var len_data_in_buf = peer.get_available_bytes()
            if len_data_in_buf > 0:
                sock.rx_data = head[1]
                var len_rx = len_data_in_buf
                if len_data_in_buf >= sock.data_len:
                    len_rx = sock.data_len
                get_tcp_data(sock, peer, len_rx)
    else:
        var len_rx = (sock.head_len + sock.data_len) - sock.rx_len
        if peer_len_data < len_rx:
            len_rx = peer_len_data
        get_tcp_data(sock, peer, len_rx)


func get_tcp_data(sock: SocketTCP, peer: StreamPeer, len_rx: int):
        var temp_data = peer.get_data(len_rx)[1]
        sock.rx_data.append_array(temp_data)
        sock.rx_len += len_rx
        if sock.rx_len == sock.head_len + sock.data_len:
            sock.rx_all = true


func _process(_delta: float) -> void:
    if Engine.is_editor_hint(): return
    tick = Time.get_ticks_msec()
    RenderingServer.global_shader_parameter_set('tick_curent', tick)
    for unit_name: StringName in units_udptx:
        var unit_udptx: = units_udptx[unit_name]
        if Error.OK == unit_udptx.process(tick):
            var sock: = send_sockets[unit_name]
            var tx_data: = unit_udptx.tx_data.slice(0, unit_udptx.tx_len)
            var dst_port: = dst_ports[unit_name]
            var dst_addr: = dst_addrs[unit_name]
            sock.send_to([dst_addr, dst_port], tx_data)
    for unit_name: StringName in units_udprx:
        var unit_udprx: = units_udprx[unit_name]
        unit_udprx.process(tick)
    for unit_name: StringName in units_serial:
        var unit_serial: = units_serial[unit_name]
        var serial: = serials[unit_name]
        serial.tick = tick
        if Error.OK == unit_serial.process(tick):
            serial.send_to(unit_serial.tx_data)
    for unit_name: StringName in units_modbus:
        var unit_modbus: = units_modbus[unit_name]
        unit_modbus.process(tick)
    for sock_unit: Array in tcp_sockets:
        var sock = sock_unit[0]
        var unit_tcp = sock_unit[1]
        if Error.OK == unit_tcp.process(tick):
            sock.send_to(unit_tcp.tx_data)
        poll_receive_tcp(sock)
    poll_sockets.any(poll_receive_udp)


## [param unit_name] - Уникальное имя устройства[br]
func get_unit_instance(unit_name: StringName) -> unit.Unit:
    return units.get(unit_name, null)
