# node_row.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 Gdk
from gi.repository import GLib
import gettext

import gtk_meshtastic_client.utils as utils
import gtk_meshtastic_client.channels_page as channels_page
import gtk_meshtastic_client.channel_row as channel_row
import gtk_meshtastic_client.chat_page as chat_page
import gtk_meshtastic_client.connection_page
import gtk_meshtastic_client.message_storage as message_storage
import gtk_meshtastic_client.position_telemetry_dialog as position_telemetry_dialog

import gtk_meshtastic_client.maps_marker as maps_marker
import gtk_meshtastic_client.maps_page as maps_page

def create_direct_message(num, longName, shortName, publicKey, reveal_page, add_to_database):
    app = Gtk.Application.get_default()
    win = Gtk.Application.get_active_window(app)

    found_dm = win.channels_page_bin.find_dm_by_id(num)
    if found_dm:
        if reveal_page:
            win.window_stack.set_visible_child_name("channel")
            win.channels_page_bin.chat_view.set_visible_child(found_dm.nav_page)
            win.channels_page_bin.split_view.set_show_content (True)

        return

    new_channel = channel_row.ChannelRow()

    if longName and longName != "":
        new_channel.set_channel_title(longName, "")
    else:
        new_channel.set_channel_title(utils.idToHex(num), "")

    new_channel.set_dm_id(num)

    if shortName and shortName != "":
        new_channel.set_index(shortName)

    if not publicKey:
        self.logger.warning("publickey is NULL! This is a programming Error")
    elif publicKey == "":
        self.logger.warning("publickey is empty! This is a programming Error")

    new_channel.set_public_key(publicKey)

    """
    Since we added a sort function, the rows will automatically sort when
    appended, no need to manually specifiy
    """
    win.channels_page_bin.dm_list_box.append(new_channel)
    win.channels_page_bin.dm_list_box_children += 1

    new_nav_page = chat_page.ChatNavPage()
    new_nav_page.set_public_key(publicKey)
    new_nav_page.set_chat_title(new_channel.title, True)
    new_nav_page.set_dm_id(num)
    new_nav_page.set_interface(win.connection_page_bin.interface)
    new_nav_page.set_send_address(num)
    win.channels_page_bin.chat_view.add_named(new_nav_page, new_channel.title)

    new_channel.set_nav_page(new_nav_page)

    if add_to_database:
        win.connection_page_bin.database.add_direct_message(num, longName, shortName, publicKey)

    if reveal_page:
        win.window_stack.set_visible_child_name("channel")
        win.channels_page_bin.chat_view.set_visible_child(new_nav_page)
        win.channels_page_bin.split_view.set_show_content (True)

@Gtk.Template(resource_path='/org/kop316/meshtastic/ui/node_row.ui')
class NodeRow(Adw.ExpanderRow):
    __gtype_name__ = 'NodeRow'

    device_info = Gtk.Template.Child()
    device_powerinfo = Gtk.Template.Child()
    other_stats = Gtk.Template.Child()
    popover = Gtk.Template.Child()
    position = Gtk.Template.Child()
    short_name_label = Gtk.Template.Child()
    snr_label = Gtk.Template.Child()
    vlt_label = Gtk.Template.Child()
    cur_label = Gtk.Template.Child()
    uptime = Gtk.Template.Child()
    public_key = Gtk.Template.Child()
    favorite_button = Gtk.Template.Child()

    lastHeard = 0
    num = 0
    devicePositionPresent = False
    deviceMetricPresent = False
    devicePowerMetricPresent = False
    isLicensed = False
    own_node = False
    favorited = False
    publicKey = "MA=="

    def set_own_node(self, own_node):
        self.own_node = own_node
        self.favorite_button.set_visible(False)

    def reset_node(self):
        self.set_title("Disconnected")
        self.set_subtitle("")
        self.other_stats.set_visible(False)
        self.device_info.set_visible(False)
        self.device_powerinfo.set_visible(False)
        self.position.set_visible(False)
        self.public_key.set_visible(False)

    def update_node_time(self):
        if hasattr(self, 'lastHeard') and self.lastHeard > 0:
            self.set_subtitle("Last Heard: " + utils.getTimeAgoDate(self.lastHeard))

    def update_node_ui(self):
        if hasattr(self, 'hwModel') and hasattr(self, 'role'):
            self.other_stats.set_title("Model: " + str(self.hwModel) + ", Role: " + str(self.role))
        elif hasattr(self, 'hwModel'):
            self.other_stats.set_title("Model: " + str(self.hwModel))
        elif hasattr(self, 'role'):
            self.other_stats.set_title("Role: " + str(self.role))


        # initializing string
        id_string = ""

        if hasattr(self, 'id'):
            id_string = id_string + "ID: " + str(self.id)
        if hasattr(self, 'longName'):
            id_string = id_string + ", Long Name: " + str(self.longName)
        if self.isLicensed:
            id_string = id_string + " (Licensed)"

        self.set_title(id_string)

        if hasattr(self, 'shortName'):
            self.short_name_label.set_label(str(self.shortName))
        else:
            self.short_name_label.set_label(" ")

        self.other_stats.set_visible(True)

        if hasattr(self, 'lastHeard') and self.lastHeard > 0:
            self.set_subtitle("Last Heard: " + utils.getTimeAgoDate(self.lastHeard))

        # initializing strings
        other_stats_string = ""
        hop_string = ""

        if hasattr(self, 'uptimeSeconds'):
            other_stats_string = other_stats_string + "Uptime: " + utils.getTimeAgoSec(self.uptimeSeconds) + ", "
        if hasattr(self, 'hopsAway'):
            other_stats_string = other_stats_string + "Hops Away: " + str(self.hopsAway)
        elif self.own_node:
            other_stats_string = other_stats_string + "Your Device"
        else:
            other_stats_string = other_stats_string + "Direct Connection"

        self.other_stats.set_subtitle(other_stats_string)

        # Generate a set of circles to visualize the hopcount
        if hasattr(self, 'hopsAway'):
            hop_string  = "\n\u25CF-" + "\u25CB-" * self.hopsAway + "\u25CF"
        elif self.own_node:
            hop_string = "\n\u25CF"
        else:
            hop_string = "\n\u25CF" + "-" + "\u25CF"

        # Set the SNR label with the SNR value and signal strength bars based on https://sensing-labs.com/f-a-q/a-good-radio-level/
        if hasattr(self, 'snr') and hasattr(self, 'rssi'):
            if self.snr >= -7 and self.rssi >= -115:
                signal_strength_bars = "████"  # 4 filled bars (strong signal)
            elif self.snr >= -7 and self.rssi >= -126 or self.snr >= -15 and self.rssi >= -115 or self.snr >= -15 and self.rssi >= -126 and self.rssi >= -115 + (self.snr + 15) * -1 * (115 - 126) / (7 - 15):
                signal_strength_bars = "███░"  # 3 filled bars (moderate signal)
            elif self.snr >= -17.5 and self.rssi >= -130 and self.snr >= -17.5 and self.rssi > -130 + (self.snr + 15) * -5 / 2.5:
                signal_strength_bars = "██░░"  # 2 filled bars (weak signal)
            elif self.snr >= -17.5 and self.rssi >= -130 and self.rssi <= -130 + (self.snr + 15) * -5 / 2.5:
                signal_strength_bars = "█░░░"  # 1 filled bar (very weak signal)
            else:
                signal_strength_bars = "░░░░"  # 1 filled bar (even weaker signal)

            self.snr_label.set_label("SNR: " + format(self.snr, '.2f') + "  " + signal_strength_bars + hop_string)
        else:
            self.snr_label.set_label(hop_string)

        self.snr_label.set_justify(Gtk.Justification.RIGHT)


        if not self.deviceMetricPresent:
            self.device_info.set_visible(False)
        else:
            self.device_info.set_visible(True)

        if hasattr(self, 'voltage') and hasattr(self, 'batteryLevel'):
            self.device_info.set_title("Voltage: " + str(self.voltage) + ", Battery Level: " + str(self.batteryLevel))
        elif hasattr(self, 'voltage'):
            self.device_info.set_title("Voltage: " + str(self.voltage))
        elif hasattr(self, 'batteryLevel'):
            self.device_info.set_title("Battery Level: " + str(self.batteryLevel))

        if hasattr(self, 'channelUtilization') and hasattr(self, 'airUtilTx'):
            self.device_info.set_subtitle("Channel Utilization: " + format(self.channelUtilization, '.2f') + ", Air Util Tx: " + format(self.airUtilTx, '.2f'))
        elif hasattr(self, 'channelUtilization'):
            self.device_info.set_subtitle("Channel Utilization: " + str(self.channelUtilization))
        elif hasattr(self, 'airUtilTx'):
            self.device_info.set_subtitle("Air Util Tx: " + format(self.airUtilTx, '.2f'))

        if not self.devicePowerMetricPresent:
            self.device_powerinfo.set_visible(False)
        else:
            self.device_powerinfo.set_visible(True)

        if hasattr(self, 'ch1Voltage') and hasattr(self, 'ch1Current'):
            if hasattr(self, 'ch2Voltage') and hasattr(self, 'ch2Current'):
                if hasattr(self, 'ch3Voltage') and hasattr(self, 'ch3Current'):
                    self.vlt_label.set_label("Ch1 Voltage: {:>10.2f}\nCh2 Voltage: {:>10.2f}\nCh3 Voltage: {:>10.2f}".format(self.ch1Voltage, self.ch2Voltage, self.ch3Voltage))
                    self.cur_label.set_label("|  Ch1 Current: {:>10.2f}\n|  Ch2 Current: {:>10.2f}\n|  Ch3 Current: {:>10.2f}".format(self.ch1Current, self.ch2Current, self.ch3Current))
                else:
                    self.vlt_label.set_label("Ch1 Voltage: {:>10.2f}\nCh2 Voltage: {:>10.2f}".format(self.ch1Voltage, self.ch2Voltage))
                    self.cur_label.set_label("|  Ch1 Current: {:>10.2f}\n|  Ch2 Current: {:>10.2f}".format(self.ch1Current, self.ch2Current))
            else:
                self.vlt_label.set_label("Ch1 Voltage: {:>10.2f}".format(self.ch1Voltage))
                self.cur_label.set_label("|  Ch1 Current: {:>10.2f}".format(self.ch1Current))


        if not self.devicePositionPresent:
            self.position.set_visible(False)
        else:
            self.position.set_visible(True)

        # https://en.wikipedia.org/wiki/Geo_URI_scheme
        # to embed a map into it
        # geo:37.786971,-122.399677;u=35
        # u is uncertainty in meters
        if hasattr(self, 'latitude') and hasattr(self, 'longitude') and hasattr(self, 'position_time'):
            self.position.set_use_markup(True)
            self.position.set_title("<a href=\"geo:" + format(self.latitude, 'f') +
                                    "," + format(self.longitude, 'f') +
                                    "\">Lat: " + format(self.latitude, 'f') +
                                    " Lon: " + format(self.longitude, 'f') +  "</a>" +
                                    ", Last Position time: " + utils.getTimeAgoDate(self.position_time))
        elif hasattr(self, 'latitude') and hasattr(self, 'longitude'):
            self.position.set_use_markup(True)
            self.position.set_title("<a href=\"geo:" + format(self.latitude, 'f') +
                                    "," + format(self.longitude, 'f') +
                                    "\">Lat: " + format(self.latitude, 'f') +
                                    " Lon: " + format(self.longitude, 'f') +  "</a>")

        elif hasattr(self, 'position_time'):
            # It doesn't really make sense to show this if Lat/Lon are not present
            self.position.set_visible(False)

        if hasattr(self, 'altitude'):
            self.position.set_subtitle("Altitude: " + str(self.altitude) + " m (" + "{:.0f}".format(self.altitude*3.28084) + " ft)")

        if hasattr(self, 'publicKey') and self.publicKey != "MA==":
            self.public_key.set_visible(True)
            self.public_key.set_subtitle("Public Key: " + str(self.publicKey))

        if hasattr(self, 'latitude') and hasattr(self, 'longitude'):
            app = Gtk.Application.get_default()
            win = Gtk.Application.get_active_window(app)
            if not hasattr(self, 'marker'):
                self.marker = maps_marker.NodeMarker()
                win.maps_page_bin.add_marker_to_map(self.marker)

            if hasattr(self, 'longName'):
                self.marker.set_marker_long_name_label(self.longName)
            else:
                self.marker.set_marker_long_name_label(self.id)

            if hasattr(self, 'position_time'):
                self.marker.set_marker_last_heard_label("Last Position Time: " + str(utils.getTimeAgoDate(self.position_time)))
            elif hasattr(self, 'lastHeard') and self.lastHeard > 0:
                self.marker.set_marker_last_heard_label("Last Heard: " + str(utils.getTimeAgoDate(self.lastHeard)))

            if self.own_node:
                self.marker.set_marker_hops_away_label("Your Device")
            elif hasattr(self, 'hopsAway'):
                self.marker.set_marker_hops_away_label("Hops Away: " + str(self.hopsAway))
            else:
                self.marker.set_marker_hops_away_label("Direct Connection")

            if hasattr(self, 'shortName'):
                self.marker.set_marker_label(self.shortName)
            else:
                self.marker.set_marker_label(self.id)

            self.marker.set_location(self.latitude, self.longitude)
            if self.own_node:
                win.maps_page_bin.goto_map_first_load(self.latitude, self.longitude)
                win.maps_page_bin.load_own_node_lat_long(self.latitude, self.longitude, self.altitude)
            else:
                win.maps_page_bin.define_longitude_last_seen(self.longitude)
                win.maps_page_bin.define_latitude_last_seen(self.latitude)

    def populate_node(self, node):
        if node.get('num'):
            self.num = node["num"]
            self.id = utils.idToHex(self.num)

        if node.get('user'):
            if node.get("user", {}).get("id"):
                self.id = node["user"]["id"]
            if node.get("user", {}).get("longName"):
                self.longName = node["user"]["longName"]
            if node.get("user", {}).get("shortName"):
                self.shortName = node["user"]["shortName"]
            if node.get("user", {}).get("hwModel"):
                self.hwModel = node["user"]["hwModel"]
            if node.get("user", {}).get("role"):
                self.role = node["user"]["role"]
            if node.get("user", {}).get("publicKey"):
                self.publicKey = node["user"]["publicKey"]
            if node.get("user", {}).get("isLicensed"):
                self.isLicensed = node["user"]["isLicensed"]

        if node.get('snr'):
            self.snr = node["snr"]

        if node.get('lastHeard'):
            self.lastHeard = node["lastHeard"]

        if node.get('hopsAway'):
            self.hopsAway = node["hopsAway"]

        if node.get('deviceMetrics'):
            self.deviceMetricPresent = True
            if node.get("deviceMetrics", {}).get("channelUtilization"):
                self.channelUtilization = node["deviceMetrics"]["channelUtilization"]

            if node.get("deviceMetrics", {}).get("batteryLevel"):
                self.batteryLevel = node["deviceMetrics"]["batteryLevel"]

            if node.get("deviceMetrics", {}).get("voltage"):
                self.voltage = node["deviceMetrics"]["voltage"]

            if node.get("deviceMetrics", {}).get("airUtilTx"):
                self.airUtilTx = node["deviceMetrics"]["airUtilTx"]

            if node.get("deviceMetrics", {}).get("uptimeSeconds"):
                self.uptimeSeconds = node["deviceMetrics"]["uptimeSeconds"]
        else:
            self.deviceMetricPresent = False

        if node.get('powerMetrics'):
            self.devicePowerMetricPresent = True
            if node.get("powerMetrics", {}).get("ch1Voltage"):
                self.ch1Voltage = node["powerMetrics"]["ch1Voltage"]

            if node.get("powerMetrics", {}).get("ch1Current"):
                self.ch1Current = node["powerMetrics"]["ch1Current"]

            if node.get("powerMetrics", {}).get("ch2Voltage"):
                self.ch2Voltage = node["powerMetrics"]["ch2Voltage"]

            if node.get("powerMetrics", {}).get("ch2Current"):
                self.ch2Current = node["powerMetrics"]["ch2Current"]

            if node.get("powerMetrics", {}).get("ch3Voltage"):
                self.ch3Voltage = node["powerMetrics"]["ch3Voltage"]

            if node.get("powerMetrics", {}).get("ch3Current"):
                self.ch3Current = node["powerMetrics"]["ch3Current"]
        else:
            self.devicePowerMetricPresent = False

        if node.get('position'):
            self.devicePositionPresent = True
            if node.get("position", {}).get("time"):
                self.position_time = node["position"]["time"]

            if (node.get("position", {}).get("latitude") and
                node.get("position", {}).get("longitude")):
                self.latitude = node["position"]["latitude"]
                self.longitude = node["position"]["longitude"]

            if (node.get("position", {}).get("latitude") and
                node.get("position", {}).get("longitude") and
                node.get("position", {}).get("altitude")):
                self.altitude = node["position"]["altitude"]
        else:
            self.devicePositionPresent = False

        self.update_node_ui()

    def update_node_with_packet(self, packet):
        self.logger.debug("Update Node: " + str(utils.idToHex(packet['from'])))
        self.num = packet["from"]
        self.id = utils.idToHex(self.num)
        self.lastHeard = utils.getUnixTimestampNow()

        if "rxSnr" in packet:
            self.snr = packet["rxSnr"]

        if "rxRssi" in packet:
            self.rssi = packet["rxRssi"]

        if 'decoded' in packet:
            if packet['decoded'].get('portnum') == 'NODEINFO_APP':
                app = Gtk.Application.get_default()
                win = Gtk.Application.get_active_window(app)

                node_info = packet['decoded'].get('user', {})
                if node_info.get("id"):
                    self.id = packet["decoded"]["user"]["id"]
                if node_info.get("longName"):
                    self.longName = packet["decoded"]["user"]["longName"]
                if node_info.get("shortName"):
                    self.shortName = packet["decoded"]["user"]["shortName"]
                if node_info.get("hwModel"):
                    self.hwModel = packet["decoded"]["user"]["hwModel"]
                if node_info.get("role"):
                    self.role = packet["decoded"]["user"]["role"]
                if node_info.get("publicKey"):
                    self.publicKey = packet["decoded"]["user"]["publicKey"]

                if node_info.get("isLicensed"):
                    self.is_licensed = packet["decoded"]["user"]["isLicensed"]

                if 'hopsAway' in packet:
                    self.hopsAway = packet["hopsAway"]

                win.connection_page_bin.database.update_direct_message_chat(self)
                if self.favorited:
                    win.connection_page_bin.database.update_favorite_node(self)

            elif packet['decoded'].get('portnum') == 'POSITION_APP':
                position = packet['decoded']['position']

                """
                If we don't have lat and long, no point in updating anything
                """
                if (position.get("latitude") and
                    position.get("longitude")):
                    self.devicePositionPresent = True
                    self.latitude = packet["decoded"]["position"]["latitude"]
                    self.longitude = packet["decoded"]["position"]["longitude"]
                else:
                    return

                if (position.get("latitude") and
                    position.get("longitude") and
                    position.get("altitude")):
                    self.altitude = packet["decoded"]["position"]["altitude"]

                if position.get("time"):
                    self.position_time = packet["decoded"]["position"]["time"]
                elif 'rxTime' in packet:
                    self.position_time = packet["rxTime"]

            elif packet.get('decoded', {}).get('portnum') == 'TELEMETRY_APP':
                telemetry = packet.get('decoded', {}).get('telemetry', {})
                device_metrics = telemetry.get('deviceMetrics', {})
                power_metrics = telemetry.get('powerMetrics', {})

                if device_metrics:
                    self.deviceMetricPresent = True
                    for key, value in device_metrics.items():
                        if value is not None:
                            setattr(self, f"{key}", value)

                if power_metrics:
                    self.devicePowerMetricPresent = True
                    for key, value in power_metrics.items():
                        if value is not None:
                            setattr(self, f"{key}", value)


    def add_favorite_node_attributes(self, num, longName, shortName, publicKey, lastHeard):
        self.num = num

        self.id = utils.idToHex(num)

        if longName and longName != "":
            self.longName = longName

        if shortName and shortName != "":
            self.shortName = shortName

        if publicKey and publicKey != "":
            self.publicKey = publicKey

        if lastHeard and lastHeard > 0:
            self.lastHeard = lastHeard

        self.update_node_ui()

    def create_direct_message_internal(self, reveal_page):
        if hasattr(self, 'longName'):
            longName = self.longName

        if hasattr(self, 'shortName'):
            shortName = self.shortName

        if hasattr(self, 'publicKey'):
            publicKey = self.publicKey
        else:
            publicKey = "MA=="

        create_direct_message(self.num, longName, shortName, publicKey, reveal_page, True)

    """self doesn't seen to work, but self is the correct widget for self"""
    def on_direct_message(self, widget, action_name, param):
        widget.create_direct_message_internal(True)

    """self doesn't seen to work, but self is the correct widget for self"""
    def on_request_position(self, widget, action_name, param):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        dialog = position_telemetry_dialog.PositionTelemetryDialog()
        dialog.set_position_or_telemetry(True)
        dialog.set_dest_id(utils.idToHex(widget.num))
        dialog.set_dialog_title("Send and Request Position")
        dialog.present(win)

    """self doesn't seen to work, but self is the correct widget for self"""
    def on_request_telmetry(self, widget, action_name, param):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        dialog = position_telemetry_dialog.PositionTelemetryDialog()
        dialog.set_position_or_telemetry(False)
        dialog.set_dest_id(utils.idToHex(widget.num))
        dialog.set_dialog_title("Send and Request Telemetry")
        dialog.present(win)

    def right_clicked_cb(self, gesture, n_press, x, y, secondary_button_gesture):
        secondary_button_gesture.set_state(Gtk.EventSequenceState.CLAIMED)
        if not self.own_node:
            self.popover.show()

    def long_pressed_cb(self, gesture, x, y, long_press_gesture):
        long_press_gesture.set_state(Gtk.EventSequenceState.CLAIMED)
        if not self.own_node:
            self.popover.show()

    def toggle_favorite_node(self, alter_database):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        """Removing a node from favorites"""
        if self.favorited:
            self.favorite_button.set_icon_name("non-starred-symbolic")
            self.favorited = False
            if alter_database:
                win.connection_page_bin.database.remove_node_from_database(self.num)
            """ Adding a node from favorites """
        else:
            self.favorite_button.set_icon_name("starred-symbolic")
            self.favorited = True

            longName = ""
            shortName = ""
            publicKey = "MA=="
            lastHeard = 0

            if hasattr(self, 'longName'):
                longName = self.longName

            if hasattr(self, 'shortName'):
                shortName = self.shortName

            if hasattr(self, 'publicKey'):
                publicKey = self.publicKey

            if hasattr(self, 'lastHeard') and self.lastHeard > 0:
                lastHeard = self.lastHeard

            if alter_database:
                win.connection_page_bin.database.add_node_to_database(self.num, longName, shortName, publicKey, lastHeard)

        win.nearby_nodes_page_bin.nearby_nodes_list_box.invalidate_sort()

    @Gtk.Template.Callback()
    def _favorite_button_clicked_cb(self, button):
        self.toggle_favorite_node(True)

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

        self.logger = app.logger

        secondary_button_gesture = Gtk.GestureClick(button=Gdk.BUTTON_SECONDARY)
        secondary_button_gesture.connect("released", self.right_clicked_cb, secondary_button_gesture)
        self.add_controller(secondary_button_gesture)

        long_press_gesture = Gtk.GestureLongPress()
        long_press_gesture.connect("pressed", self.long_pressed_cb, long_press_gesture)
        self.add_controller(long_press_gesture)

        self.popover.set_parent(self.short_name_label)
        self.install_action("node_row.direct-message", None, self.on_direct_message)
        self.install_action("node_row.request-position", None, self.on_request_position)
        self.install_action("node_row.request-telmetry", None, self.on_request_telmetry)
