#!/usr/bin/python3

# This file is part of DarkFi (https://dark.fi)
#
# Copyright (C) 2020-2024 Dyne.org foundation
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import asyncio, random, re, sys, base58, json
import base58
import urwid as u
import networkx as nx
# import matplotlib.pyplot as plt
import src.util

from os.path import join

# # Create a directed graph
# dag = nx.DiGraph()

# # Add edges to the graph (this also adds nodes)
# dag.add_edges_from([
#     ("root", "a"),
#     ("a", "b"),
#     ("a", "e"),
#     ("b", "c"),
#     ("b", "d"),
#     ("d", "e")
# ])

class JsonRpc:

    async def start(self, server, port):
        reader, writer = await asyncio.open_connection(server, port, limit=1024 * 128)
        self.reader = reader
        self.writer = writer

    async def stop(self):
        self.writer.close()
        await self.writer.wait_closed()

    async def _make_request(self, method, params):
        ident = random.randint(0, 2**16)
        #print(ident)
        request = {
            "jsonrpc": "2.0",
            "method": method,
            "params": params,
            "id": ident,
        }

        message = json.dumps(request) + "\n"
        self.writer.write(message.encode())
        await self.writer.drain()

        data = await self.reader.readline()
        message = data.decode().strip()
        response = json.loads(message)
        #print(response)
        return response

    async def _subscribe(self, method, params):
        ident = random.randint(0, 2**16)
        request = {
            "jsonrpc": "2.0",
            "method": method,
            "params": params,
            "id": ident,
        }

        message = json.dumps(request) + "\n"
        self.writer.write(message.encode())
        await self.writer.drain()
        #print("Subscribed")

    async def ping(self):
        return await self._make_request("ping", [])

    async def dnet_switch(self, state):
        return await self._make_request("dnet.switch", [state])

    async def dnet_subscribe_events(self):
        return await self._subscribe("dnet.subscribe_events", [])
    
    async def deg_switch(self, state):
        return await self._make_request("deg.switch", [state])


class ListItem(u.WidgetWrap):
    
    def __init__ (self, event):
        self.content = event
        layer_num = int(event["layer"])
        layer = "layer " + str(layer_num) if layer_num != 0 else "genesis"
        t = u.AttrWrap(u.Text(layer), "event", "event_selected")
        u.WidgetWrap.__init__(self, t)

    def selectable (self):
        return True
    
    def keypress(self, size, key):
        return key

class ListView(u.WidgetWrap):

    def __init__(self):
        u.register_signal(self.__class__, ['show_details'])
        self.walker = u.SimpleFocusListWalker([])
        lb = u.ListBox(self.walker)
        u.WidgetWrap.__init__(self, lb)

    def modified(self):
        focus_w, _ = self.walker.get_focus()
        u.emit_signal(self, 'show_details', focus_w.content)

    def set_data(self, events):
        events_widgets = [ListItem(e) for e in events]
        u.disconnect_signal(self.walker, 'modified', self.modified)

        while len(self.walker) > 0:
            self.walker.pop()
        
        self.walker.extend(events_widgets)
        u.connect_signal(self.walker, "modified", self.modified)
        self.walker.set_focus(0)

class DetailView(u.WidgetWrap):
    
    def __init__ (self):
        t = u.Text("")
        u.WidgetWrap.__init__(self, t)
        
    def set_event(self, c):
        s = f'Hash: {c["hash"]}\nChildren: {c["children"]}\nContent: {c["content"]}\nLayer: {c["layer"]}'
        self._w.set_text(s)

class App(object):
    
    def unhandled_input(self, key):
        if key in ('q',):
            raise u.ExitMainLoop()

    def show_details(self, event):
        self.detail_view.set_event(event)
        
    def __init__(self):
        self.palette = {
            ("bg",               "white",       "black"),
            ("event",            "white",       "black"),
            ("event_selected",   "white",       "yellow"),
            ("footer",           "white, bold", "dark red")
        }

        self.list_view = ListView()
        self.detail_view = DetailView()
        u.connect_signal(self.list_view, 'show_details', self.show_details)
        footer = u.AttrWrap(u.Text(" Q to exit"), "footer") 
        col_rows = u.raw_display.Screen().get_cols_rows()
        h = col_rows[0] - 2
        f1 = u.Filler(self.list_view, valign='top', height=h)
        f2 = u.Filler(self.detail_view, valign='top')
        c_list = u.LineBox(f1, title="Layers")
        c_details = u.LineBox(f2, title="Details")
        columns = u.Columns([('weight', 15, c_list), ('weight', 85, c_details)])            
        frame = u.AttrMap(u.Frame(body=columns, footer=footer), 'bg')
        self.loop = u.MainLoop(frame, self.palette, unhandled_input=self.unhandled_input)

    async def update_data(self, config):
        host = config['host']
        port = config['port']
        rpc = JsonRpc()
        while True:
            try:
                await rpc.start(host, port)
                break
            except OSError:
                print("Error: Couldn't connent to rpc")
                exit(-1)

        await rpc.deg_switch(True)
        await rpc.deg_switch(False)

        json_result = await rpc._make_request("eventgraph.get_info", [])
        
        if json_result['result']['eventgraph_info']:
            dag_dict = json_result['result']['eventgraph_info']['dag']
            dag_list = list(dag_dict.items())
            # sorted_dag = sorted(dag_list, key=lambda x:x[1]['layer'])

            # genesis_hash = sorted_dag[0][0]

            parent_child_pairs = []
            for item in dag_list:
                parents = item[1]['parents']
                child = item[0]
                for parent in parents:
                    if parent == '0' * 64:
                        continue
                    parent_child_pairs.append((parent, child))
                    
            # Create a directed graph
            dag = nx.DiGraph()

            # Add edges to the graph (this also adds nodes)
            dag.add_edges_from(parent_child_pairs)
            l = []
            topological_order = list(nx.topological_sort(dag))
            for node in topological_order:
                event_details = dag_dict.get(node) # details
                layer = int(event_details['layer'])
                content = event_details['content'] # event content
                # print(content)
                pattern = r'\\x[0-9A-Fa-f]{2}'
                decoded_str = str(base58.b58decode(content))
                matches = re.split(pattern, decoded_str)
                children = dag.successors(node)
                l.append({"layer":f"{layer}", "hash":f"{node}", "children":f"{list(children)}", "content":f"{matches[1:]}"})
            
            self.list_view.set_data(l)

    async def start(self, config):
        await self.update_data(config)
        self.loop.run()

async def main(argv):

    os = src.util.get_os()
    config_path = src.util.user_config_dir('darkfi', os)

    suffix = '.toml'
    filename = 'deg_config'
    path = join(config_path, filename + suffix)
    config = src.util.spawn_config(path)
    config = config['nodes'][0]

    if len(argv) > 1:
        if argv[1] in ['darkirc', 'irc']:
            config['port'] = 26660
        elif argv[1] in ['taud', 'tau']:
            config['port'] = 23330

    app = App()
    await app.start(config)
    

asyncio.run(main(sys.argv))
