# device_connection_page.py
#
# Copyright 2024 Christopher Talbot
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
import json
import gettext
import pyqrcode
import meshtastic.serial_interface
import meshtastic.tcp_interface
import meshtastic.mesh_interface
import meshtastic.ble_interface
import meshtastic
from meshtastic.util import fromPSK
from pubsub import pub
import os

import gtk_meshtastic_client.bluetooth_device_row as bluetooth_device_row
import gtk_meshtastic_client.message_parser as message_parser
import gtk_meshtastic_client.message_storage as message_storage
import gtk_meshtastic_client.nearby_nodes_page as nearby_nodes_page
import gtk_meshtastic_client.utils as utils

@Gtk.Template(resource_path='/org/kop316/meshtastic/ui/device_configuration_page.ui')
class DeviceConfigurationPageBin(Adw.Bin):
    __gtype_name__ = 'DeviceConfigurationPageBin'

    all_channels_button = Gtk.Template.Child()
    apply_long_short_name_button = Gtk.Template.Child()
    apply_wifi_settings_button = Gtk.Template.Child()
    bluetooth_settings_expander_row = Gtk.Template.Child()
    bluetooth_enabled_switch = Gtk.Template.Child()
    bluetooth_pin_type = Gtk.Template.Child()
    bluetooth_pin_entry = Gtk.Template.Child()
    channel_expander_row = Gtk.Template.Child()
    channel_config_button = Gtk.Template.Child()
    channel_qr = Gtk.Template.Child()
    channels_url_entry = Gtk.Template.Child()
    configure_channels_url_entry = Gtk.Template.Child()
    configure_channels_url_entry_button = Gtk.Template.Child()
    ethernet_enabled_switch = Gtk.Template.Child()
    lisensed_row = Gtk.Template.Child()
    long_name_entry = Gtk.Template.Child()
    long_name_entry_button = Gtk.Template.Child()
    network_settings_expander_row = Gtk.Template.Child()
    node_name_expander_row = Gtk.Template.Child()
    ntp_server_entry = Gtk.Template.Child()
    role_selection = Gtk.Template.Child()
    primary_channel_button = Gtk.Template.Child()
    short_name_entry = Gtk.Template.Child()
    short_name_entry_button = Gtk.Template.Child()
    qr_code = Gtk.Template.Child()
    wifi_ssid_entry = Gtk.Template.Child()
    wifi_password_entry = Gtk.Template.Child()
    wifi_enabled_switch = Gtk.Template.Child()

    telemetry_settings_expander_row = Gtk.Template.Child()
    power_metrics_enabled_switch = Gtk.Template.Child()

    dhcp_enabled_switch = Gtk.Template.Child()
    ipv4_config_ip = Gtk.Template.Child()
    ipv4_config_gateway = Gtk.Template.Child()
    ipv4_config_subnet = Gtk.Template.Child()
    ipv4_config_dns = Gtk.Template.Child()

    disconnection_timeout = 10000 #milliseconds

    def populate_entries(self, interface, reset_expanders):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        self.interface = interface

        node = interface.getNode('^local')

        if reset_expanders:
            self.network_settings_expander_row.set_expanded(False)
            self.node_name_expander_row.set_expanded(False)
            self.channel_expander_row.set_expanded(False)
            self.bluetooth_settings_expander_row.set_expanded(False)
            self.telemetry_settings_expander_row.set_expanded(False)

        self._clear_channel_entries()
        self.long_name_entry.set_text(win.nearby_nodes_page_bin.own_node.longName)
        self.short_name_entry.set_text(win.nearby_nodes_page_bin.own_node.shortName)
        self.lisensed_row.set_active(win.nearby_nodes_page_bin.own_node.isLicensed)
        self.set_connected_button_sensitivity(True)

        self.ethernet_enabled_switch.set_active(node.localConfig.network.eth_enabled)
        self.wifi_ssid_entry.set_text(node.localConfig.network.wifi_ssid)
        self.wifi_password_entry.set_text(node.localConfig.network.wifi_psk)
        self.ntp_server_entry.set_text(node.localConfig.network.ntp_server)

        self.wifi_enabled_switch.set_active(node.localConfig.network.wifi_enabled)
        if node.localConfig.network.wifi_enabled:
            self.wifi_ssid_entry.set_text(node.localConfig.network.wifi_ssid)
            self.wifi_password_entry.set_text(node.localConfig.network.wifi_psk)
            self.wifi_ssid_entry.set_sensitive(True)
            self.wifi_password_entry.set_sensitive(True)
        else:
            self.wifi_ssid_entry.set_text("")
            self.wifi_password_entry.set_text("")
            self.wifi_ssid_entry.set_sensitive(False)
            self.wifi_password_entry.set_sensitive(False)

        if node.localConfig.network.ipv4_config.ip == 0:
            self.dhcp_enabled_switch.set_active(True)
            self.ipv4_config_ip.set_text("")
            self.ipv4_config_gateway.set_text("")
            self.ipv4_config_subnet.set_text("")
            self.ipv4_config_dns.set_text("")
            self.ipv4_config_ip.set_sensitive(False)
            self.ipv4_config_gateway.set_sensitive(False)
            self.ipv4_config_subnet.set_sensitive(False)
            self.ipv4_config_dns.set_sensitive(False)
        else:
            self.dhcp_enabled_switch.set_active(False)
            self.ipv4_config_ip.set_text(node.localConfig.network.ipv4_config.ip)
            self.ipv4_config_gateway.set_text(node.localConfig.network.ipv4_config.gateway)
            self.ipv4_config_subnet.set_text(node.localConfig.network.ipv4_config.subnet)
            self.ipv4_config_dns.set_text(node.localConfig.network.ipv4_config.dns)
            self.ipv4_config_ip.set_sensitive(True)
            self.ipv4_config_gateway.set_sensitive(True)
            self.ipv4_config_subnet.set_sensitive(True)
            self.ipv4_config_dns.set_sensitive(True)

        self.bluetooth_enabled_switch.set_active(node.localConfig.bluetooth.enabled)
        self.bluetooth_pin_type.set_selected(node.localConfig.bluetooth.mode)
        self.bluetooth_pin_entry.set_text(str(node.localConfig.bluetooth.fixed_pin).zfill(6))

        self.bluetooth_pin_type.set_sensitive(node.localConfig.bluetooth.enabled)
        self.bluetooth_pin_entry.set_sensitive(node.localConfig.bluetooth.enabled)

        self.role_selection.set_selected(node.localConfig.device.role)

        self.power_metrics_enabled_switch.set_active(node.moduleConfig.telemetry.power_measurement_enabled)

    def set_connected_button_sensitivity(self, sensitivity):
        not_sensitivity = not sensitivity
        #Set sensitive when connected
        self.all_channels_button.set_sensitive(sensitivity)
        self.configure_channels_url_entry_button.set_sensitive(sensitivity)
        self.primary_channel_button.set_sensitive(sensitivity)
        self.apply_long_short_name_button.set_sensitive(sensitivity)
        self.long_name_entry_button.set_sensitive(sensitivity)
        self.short_name_entry_button.set_sensitive(sensitivity)
        self.lisensed_row.set_sensitive(sensitivity)
        self.channel_config_button.set_sensitive(sensitivity)
        self.apply_wifi_settings_button.set_sensitive(sensitivity)
        self.wifi_enabled_switch.set_sensitive(sensitivity)
        self.wifi_ssid_entry.set_sensitive(sensitivity)
        self.wifi_password_entry.set_sensitive(sensitivity)
        self.ethernet_enabled_switch.set_sensitive(sensitivity)
        self.dhcp_enabled_switch.set_sensitive(sensitivity)
        #Set sensitive when disconnected

    def _clear_channel_entries(self):
        self.qr_code.set_visible(False)
        self.channels_url_entry.set_text("")
        self.configure_channels_url_entry.set_text("")

    def _get_channel_url(self, interface, IncludeAll):
        node = interface.getNode('^local')
        url = node.getURL(IncludeAll)
        return url

    def _display_channel_url(self, url, file_path):
        self.channels_url_entry.set_text("")
        self.channels_url_entry.set_text(url)
        url_qr = pyqrcode.create(url)
        url_qr.svg(file_path, scale=8, background="white")
        self.channel_qr.set_filename(file_path)
        self.channel_qr.set_size_request(300, 300)
        self.qr_code.set_visible(True)

    @Gtk.Template.Callback()
    def _show_primary_channel_clicked_cb(self, button):
        url = self._get_channel_url(self.interface, False)

        qr_location = GLib.get_tmp_dir() + "/qr_primary_channel.svg"
        self._display_channel_url(url, qr_location)

    @Gtk.Template.Callback()
    def _show_all_channels_clicked_cb(self, button):
        url = self._get_channel_url(self.interface, True)

        qr_location = GLib.get_tmp_dir() + "/qr_all_channels.svg"
        self._display_channel_url(url, qr_location)

    @Gtk.Template.Callback()
    def _copy_shared_channel_clicked_cb(self, button):
        clipboard = button.get_clipboard()
        clipboard.set(self.channels_url_entry.get_text())

    @Gtk.Template.Callback()
    def _clear_channels_clicked_cb(self, button):
        self._clear_channel_entries()

    def disconnection_event_cb(self) -> bool:
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)

        if not hasattr(win.connection_page_bin, 'interface'):
            self.logger.debug("Don't have interface anymore, Not going to force disconnect")
            return GLib.SOURCE_REMOVE

        self.logger.debug("Disconnecting device")
        win.connection_page_bin.onDisconnection(win.connection_page_bin.interface)
        return GLib.SOURCE_REMOVE

    def _add_channel_entry_callback(self, dialog, response, user):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)

        if "continue" == response:
            new_url = self.configure_channels_url_entry.get_text()
            node = self.interface.getNode('^local')
            node.setURL(new_url)

            win.channels_page_bin.reset_all_channels_and_dms()
            win.channels_page_bin.populate_all_channels_and_dms(self.interface)
            GLib.timeout_add(self.disconnection_timeout, self.disconnection_event_cb)

    @Gtk.Template.Callback()
    def _add_channel_entry_apply_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        dialog = Adw.AlertDialog.new(_("Alert"),
                                     _("This will delete your current channel configuration "
                                       "continue?"))
        dialog.add_response ("close",  _("_Close"))
        dialog.add_response ("continue",  _("_Continue"))

        dialog.set_response_appearance ("continue",
                                        Adw.ResponseAppearance.DESTRUCTIVE)

        dialog.connect("response", self._add_channel_entry_callback, self)

        dialog.present (win)

    @Gtk.Template.Callback()
    def _apply_long_short_name_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)

        node = self.interface.getNode('^local')
        short_name = self.short_name_entry.get_text()
        long_name = self.long_name_entry.get_text()
        is_licensed = self.lisensed_row.get_active()
        if is_licensed:
            node.channels[0].settings.psk = fromPSK("none")
            self.logger.debug("Disabling Encryption on Primary Channel")
            node.writeChannel(0)
        node.setOwner(long_name, short_name, is_licensed)

        win.window_toast_overlay.add_toast(Adw.Toast(title="Name Configuration Complete"))
        GLib.timeout_add(self.disconnection_timeout, self.disconnection_event_cb)

    @Gtk.Template.Callback()
    def _apply_network_settings_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)

        node = self.interface.getNode('^local')
        node.localConfig.network.eth_enabled = self.ethernet_enabled_switch.get_active()
        node.localConfig.network.ntp_server = self.ntp_server_entry.get_text()

        node.localConfig.network.wifi_enabled = self.wifi_enabled_switch.get_active()
        if self.wifi_enabled_switch.get_active():
            node.localConfig.network.wifi_ssid = self.wifi_ssid_entry.get_text()
            node.localConfig.network.wifi_psk = self.wifi_password_entry.get_text()
        else:
            node.localConfig.network.wifi_ssid = 0
            node.localConfig.network.wifi_psk = 0


        if self.dhcp_enabled_switch.get_active():
            node.localConfig.network.ipv4_config.ip = 0
            node.localConfig.network.ipv4_config.gateway = 0
            node.localConfig.network.ipv4_config.subnet = 0
            node.localConfig.network.ipv4_config.dns = 0
        else:
            node.localConfig.network.ipv4_config.ip = self.ipv4_config_ip.get_text()
            node.localConfig.network.ipv4_config.gateway = self.ipv4_config_gateway.get_text()
            node.localConfig.network.ipv4_config.subnet = self.ipv4_config_subnet.get_text()
            node.localConfig.network.ipv4_config.dns = self.ipv4_config_dns.get_text()

        node.writeConfig("network")

        win.window_toast_overlay.add_toast(Adw.Toast(title="Network Settings Applied"))
        GLib.timeout_add(self.disconnection_timeout, self.disconnection_event_cb)

    @Gtk.Template.Callback()
    def wifi_switch_row_notify_active_cb(self, switch_row, user):
        set_sensitive = switch_row.get_active()
        self.wifi_ssid_entry.set_sensitive(set_sensitive)
        self.wifi_password_entry.set_sensitive(set_sensitive)

        if not set_sensitive:
            self.wifi_ssid_entry.set_text("")
            self.wifi_password_entry.set_text("")

    @Gtk.Template.Callback()
    def dhcp_switch_row_notify_active_cb(self, switch_row, user):
        set_sensitive = not switch_row.get_active()
        self.ipv4_config_ip.set_sensitive(set_sensitive)
        self.ipv4_config_gateway.set_sensitive(set_sensitive)
        self.ipv4_config_subnet.set_sensitive(set_sensitive)
        self.ipv4_config_dns.set_sensitive(set_sensitive)

        if not set_sensitive:
            self.ipv4_config_ip.set_text("")
            self.ipv4_config_gateway.set_text("")
            self.ipv4_config_subnet.set_text("")
            self.ipv4_config_dns.set_text("")

    @Gtk.Template.Callback()
    def _reset_page_button_clicked_cb(self, button):
        self.populate_entries(self.interface, False)

    def _device_config_reset_callback(self, dialog, response, user):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        if "continue" != response:
            return

        node = self.interface.getNode('^local')
        node.factoryReset(full=False)

        win.window_toast_overlay.add_toast(Adw.Toast(title="Device Configuration Reset Complete"))
        return

    @Gtk.Template.Callback()
    def _reset_device_config_button_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        dialog = Adw.AlertDialog.new(_("Alert"),
                                     _("This resets the device configuration and cannot be undone!"))
        dialog.add_response ("close",  _("_Close"))
        dialog.add_response ("continue",  _("_Continue"))

        dialog.set_response_appearance ("continue",
                                        Adw.ResponseAppearance.DESTRUCTIVE)

        dialog.connect("response", self._device_config_reset_callback, self)

        dialog.present(win)

    def _factory_reset_callback(self, dialog, response, user):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        if "continue" != response:
            return

        node = self.interface.getNode('^local')
        node.factoryReset(full=True)

        win.window_toast_overlay.add_toast(Adw.Toast(title="Factory Reset Complete"))
        return

    @Gtk.Template.Callback()
    def _factory_reset_device_button_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        dialog = Adw.AlertDialog.new(_("Alert"),
                                     _("This factory resets the device (including device keys) and cannot be undone!"))
        dialog.add_response ("close",  _("_Close"))
        dialog.add_response ("continue",  _("_Continue"))

        dialog.set_response_appearance ("continue",
                                        Adw.ResponseAppearance.DESTRUCTIVE)

        dialog.connect("response", self._factory_reset_callback, self)

        dialog.present(win)

    def _reset_node_db_callback(self, dialog, response, user):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        if "continue" != response:
            return

        node = self.interface.getNode('^local')
        node.resetNodeDb()

        win.window_toast_overlay.add_toast(Adw.Toast(title="Node Database Deleted"))
        return

    @Gtk.Template.Callback()
    def _reset_node_db_button_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        dialog = Adw.AlertDialog.new(_("Alert"),
                                     _("This deletes the Node Database and cannot be undone!"))
        dialog.add_response ("close",  _("_Close"))
        dialog.add_response ("continue",  _("_Continue"))

        dialog.set_response_appearance ("continue",
                                        Adw.ResponseAppearance.DESTRUCTIVE)

        dialog.connect("response", self._reset_node_db_callback, self)

        dialog.present(win)

    @Gtk.Template.Callback()
    def _reset_long_name_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        self.long_name_entry.set_text(win.nearby_nodes_page_bin.own_node.longName)

    @Gtk.Template.Callback()
    def _reset_short_name_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        self.short_name_entry.set_text(win.nearby_nodes_page_bin.own_node.shortName)

    @Gtk.Template.Callback()
    def _channel_config_button_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        win.connection_page_bin.channel_configuration_page.reset_channels()
        win.connection_page_bin.connection_page_nav_view.push_by_tag("channel-configuration")

    @Gtk.Template.Callback()
    def _apply_bluetooth_settings_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        node = self.interface.getNode('^local')

        enable_bluetooth = self.bluetooth_enabled_switch.get_active()
        pin_type = self.bluetooth_pin_type.get_selected()
        pin_entry = self.bluetooth_pin_entry.get_text()
        try:
            int_pin_entry = int(pin_entry)
        except:
            self.logger.warning("Illegal Chars in PIN Entry!")
            dialog = Adw.AlertDialog.new(_("Alert"),
                                         _("Only put numbers in PIN Entry"))
            dialog.add_response ("close",  _("_Close"))

            dialog.present (win)
            return

        self.logger.debug("BLE Enabled: " + str(enable_bluetooth))
        self.logger.debug("Pin type: " + str(pin_type))
        self.logger.debug("Fixed PIN: " + str(int_pin_entry))

        node.localConfig.bluetooth.enabled = enable_bluetooth
        node.localConfig.bluetooth.mode = pin_type
        node.localConfig.bluetooth.fixed_pin = int_pin_entry

        self.logger.debug("Applying BLE Settings")
        win.window_toast_overlay.add_toast(Adw.Toast(title="Bluetooth Settings Applied"))
        GLib.timeout_add(self.disconnection_timeout, self.disconnection_event_cb)

        node.writeConfig("bluetooth")

    @Gtk.Template.Callback()
    def _apply_telemetry_settings_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        node = self.interface.getNode('^local')

        enable_power_metrics = self.power_metrics_enabled_switch.get_active()

        self.logger.debug("Power Metrics Enabled: " + str(enable_power_metrics))

        node.moduleConfig.telemetry.power_measurement_enabled = enable_power_metrics

        self.logger.debug("Applying Telemetry Settings")
        win.window_toast_overlay.add_toast(Adw.Toast(title="Telemetry Settings Applied"))
        GLib.timeout_add(self.disconnection_timeout, self.disconnection_event_cb)

        node.writeConfig("telemetry")

    @Gtk.Template.Callback()
    def _bluetooth_enabled_switch_row_notify_active_cb(self, switch_row, user):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        self.logger.debug("BLE Enabled switch row switched")

        self.bluetooth_pin_type.set_sensitive(self.bluetooth_enabled_switch.get_active())
        self.bluetooth_pin_entry.set_sensitive(self.bluetooth_enabled_switch.get_active())

    @Gtk.Template.Callback()
    def _set_device_role_button_clicked_cb(self, button):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        node = self.interface.getNode('^local')

        role_selection = self.role_selection.get_selected()

        node.localConfig.device.role = role_selection
        node.writeConfig("device")

        self.logger.debug("Applying Device Role Settings")
        win.window_toast_overlay.add_toast(Adw.Toast(title="Device Role Setting Applied"))
        GLib.timeout_add(self.disconnection_timeout, self.disconnection_event_cb)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        app = Gtk.Application.get_default()
        self.logger = app.logger
