#####################################################################
#                                                                   #
#  THIS IS A SOURCE CODE FILE FROM A PROGRAM TO INTERACT WITH THE   #
# LBRY PROTOCOL ( lbry.com ). IT WILL USE THE LBRY SDK ( lbrynet )  #
# FROM THEIR REPOSITORY ( https://github.com/lbryio/lbry-sdk )      #
# WHICH I GONNA PRESENT TO YOU AS A BINARY. SINCE I DID NOT DEVELOP #
# IT AND I'M LAZY TO INTEGRATE IN A MORE SMART WAY. THE SOURCE CODE #
# OF THE SDK IS AVAILABLE IN THE REPOSITORY MENTIONED ABOVE.        #
#                                                                   #
#      ALL THE CODE IN THIS REPOSITORY INCLUDING THIS FILE IS       #
# (C) J.Y.Amihud and Other Contributors 2021. EXCEPT THE LBRY SDK.  #
# YOU CAN USE THIS FILE AND ANY OTHER FILE IN THIS REPOSITORY UNDER #
# THE TERMS OF GNU GENERAL PUBLIC LICENSE VERSION 3 OR ANY LATER    #
# VERSION. TO FIND THE FULL TEXT OF THE LICENSE GO TO THE GNU.ORG   #
# WEBSITE AT ( https://www.gnu.org/licenses/gpl-3.0.html ).         #
#                                                                   #
# THE LBRY SDK IS UNFORTUNATELY UNDER THE MIT LICENSE. IF YOU ARE   #
# NOT INTENDING TO USE MY CODE AND JUST THE SDK. YOU CAN FIND IT ON #
# THEIR OFFICIAL REPOSITORY ABOVE. THEIR LICENSE CHOICE DOES NOT    #
# SPREAD ONTO THIS PROJECT. DON'T GET A FALSE ASSUMPTION THAT SINCE #
# THEY USE A PUSH-OVER LICENSE, I GONNA DO THE SAME. I'M NOT.       #
#                                                                   #
# THE LICENSE CHOSEN FOR THIS PROJECT WILL PROTECT THE 4 ESSENTIAL  #
# FREEDOMS OF THE USER FURTHER, BY NOT ALLOWING ANY WHO TO CHANGE   #
# THE LICENSE AT WILL. SO NO PROPRIETARY SOFTWARE DEVELOPER COULD   #
# TAKE THIS CODE AND MAKE THEIR USER-SUBJUGATING SOFTWARE FROM IT.  #
#                                                                   #
#####################################################################

# Since LBRY wallet is just a long stream of increasing and decreasing
# numbers. I call this the analytics.

import os
import math
import json
import time
import random
import colorsys
import threading

from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import cairo

from flbry import fetch
from flbry import settings





def make_colors(amount=4, color=(0.1,0.5,0.8)):

    # TODO: Figure out how to got the themes prefered active color
    # and use it here. 
    
    colors = []
    for i in range(amount):

        nc = colorsys.rgb_to_hsv(*color)
        nc = list(nc)
        
        if i != 0:
            nc[1] =  0
            nc[2] = (i)*(1/amount)
            
        colors.append(colorsys.hsv_to_rgb(*nc))

    return colors

def window(win):

    

    
    awin = Gtk.Window()
    awin.set_title("FastLBRY GTK: Wallet / Analytics")
    awin.set_size_request(500, 500)  


    notebook = Gtk.Notebook()
    notebook.set_scrollable(True)
    awin.add(notebook)

    ############################ ANALYTICS SECTION #############################

    box = Gtk.VBox()
    notebook.append_page(box, Gtk.Label("Totals / Analytics"))
    
    # Let's get...
    raw_balance = fetch.lbrynet("wallet_balance")

    # ... and format the balance information.
    balance = {
        "Spendable":raw_balance["available"],
        "Claim Bids":raw_balance["reserved_subtotals"]["claims"],
        "Supports":raw_balance["reserved_subtotals"]["supports"],
        "Tips":raw_balance["reserved_subtotals"]["tips"],
    }



    # Pie chart for balance
    def conv(t):
        return str(t) + " LBC"
    box.pack_start(pie_chart(win, balance, "Totals", converter=conv), 0, 0 , 0)

    minus_30_days = 60*60*24*30*-1
    
    transactions = {"items":[],       # The list of items
                    "zoom":[0,0], #minus_30_days,0], # The range of the selection ( by timestamp )
                    "allow_negative":True
    }
    
    def getting_all_transactions(data, win):
        
        # This is a small function funnin in a separate thread, loading
        # all transactions so the graph could draw them.

        # TODO: this commented code works perfectly fine to get the kind of
        # data that I want. But it's hell'a heavy on my computer. Last time
        # I tried, it froze. So I need to figure out something about this.

        # cash_file = settings.get_settings_folder()+"GTK-transactions-graph.json"
        
        # try:
        #     with open(cash_file) as json_file: 
        #         data["items"] = json.load(json_file)
        # except Exception as e:
        #     print(e)

            
            
        # def add(out, addpage):
        #     ret = False
        #     added = 0
        #     count = 0
        #     for i in out["items"]:

        #         # Removing unnesesary data
                
        #         a = i.copy()
        #         # a["amount"] = i["amount"]
        #         # a["timestamp"] = i["timestamp"]
        #         if a["timestamp"] == None: # Too recent
        #             a["timestamp"] = int(time.time())
        #         # a["txid"] = i["txid"]
        #         # a["name"] = i.get("name", "")

        #         if a not in data["items"]:
        #             added += 1
        #             data["items"].append(a)
        #         elif not addpage:
        #             count += 1
        #             ret = True

        #     return ret
                

        # Fetching

        
        
        # out = fetch.lbrynet("transaction_list", { "page_size":50})
        # all_of_them = out["total_items"]
        # addpage = 0
        # add(out, 0)
        # for i in range(out["total_pages"]):
        #     out = fetch.lbrynet("transaction_list", {"page":addpage+i+2,
        #                                      #"exclude_internal_transfers": True,
        #                                      #"no_totals":True,
        #                                      "page_size":50})
            
        #     if add(out, addpage):
        #         addpage = int((len(data["items"]))/50) - 2
                      

        #     # Save to cash
        #     with open(cash_file, 'w') as fp:
        #          json.dump(data["items"], fp)

        #     if not win.keep_loading_the_wallet_graph or all_of_them == len(data["items"]):
        #         return

        # THE MEANWHILE SOLUTION

         out = fetch.lbrynet("txo_plot", { "days_back":1000, # Fetch 100 days of txo
                                           "exclude_internal_transfers":True, # Without crap
                                           "is_not_my_input":True, # Not from me ( as in support only )
         })


         
         for i in out:

             a = {}
             a["amount"] = i["total"]
             a["timestamp"] = int(time.mktime(time.strptime(i["day"],"%Y-%m-%d")))


             data["items"].append(a)
        

        
    win.keep_loading_the_wallet_graph = True
    t = threading.Thread(target=getting_all_transactions, args=(transactions, win))
    t.setDaemon(True)
    t.start()
    def kill_graph(w):
        win.keep_loading_the_wallet_graph = False
    awin.connect("destroy", kill_graph)
        
    # Graph with the history of all transactions
    the_graph = graph(win, transactions, "Totals")
    
    box.pack_start(the_graph, 1, 1 , 0)
    
    
    
    awin.show_all()

def box_of_color(color):

    def on_draw(widget, cr, data):
        
        width = widget.get_allocated_width()
        height = widget.get_allocated_height()
        
        cr.set_source_rgb(*color)
        cr.rectangle(0, 0, width, height)
        cr.fill()
    
    area = Gtk.DrawingArea()
    area.set_size_request(40, 40)
    area.connect("draw", on_draw, color)
    return area
    
def pie_chart_draw(d, main_layer, win, data, colors):

    
    # Need to know how big is our chart
    w = d.get_allocated_width()
    h = d.get_allocated_height()

    # We want our circle to fit, so we find which one is
    # smaller.
    smaller = min(w, h)

    

    last = 0
    whole = float(math.pi*2)

    sum_of_data = 0
    
    for i in data:
        sum_of_data += float(data[i])
    
    for n, i in enumerate(data):
        
        this = whole* ( float(data[i]) / sum_of_data )
        
        main_layer.move_to(w/2, h/2)
        main_layer.arc(w/2, h/2, smaller/2, last, last+this)
        main_layer.close_path()
        main_layer.set_source_rgb(*colors[n%len(colors)])
        main_layer.fill()
        
        last = last+this

def pie_chart(win, data, title="", converter=False):

    ret = Gtk.HBox(True)

    colors = make_colors(len(data))
    
    da = Gtk.DrawingArea()
    da.connect("draw", pie_chart_draw, win, data, colors)

    ret.pack_start(da, 1,1,5)

    lbox = Gtk.VBox()
    ret.pack_start(lbox, 1,1,5)

    sum_of_data = 0
    
    for i in data:
        sum_of_data += float(data[i])

    if converter:
       sum_of_data = converter(sum_of_data) 
        
    lbox.pack_start(Gtk.Label("  Total : "+str(sum_of_data)+"  "), 0,0,1)
    
    for n, i in enumerate(data):
        ibox = Gtk.HBox()
        lbox.pack_start(ibox, 0,0,3)

        ibox.pack_start(box_of_color(colors[n%len(colors)]), 0,0,0)
        show_size = data[i]
        if converter:
            show_size = converter(show_size) 
        ibox.pack_start(Gtk.Label("  "+i+": "+str(show_size)+"  "), 0,0,1)
                        
        
    
    return ret

def graph_draw(d, main_layer, win, data, currancy, graph_addition):

        
    data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
    
    # Need to know how big is our graph
    w = d.get_allocated_width()
    h = d.get_allocated_height()

    if data.get("allow_negative", False):
        zero_at = h / 2
    else:
        zero_at = h

    # The mouse position of a given frame
    mx = d.get_pointer()[0]
    my = d.get_pointer()[1]

    # Test of the mouse position
    # main_layer.move_to(0,0)
    # main_layer.line_to(mx, my)
    # main_layer.stroke()

    
    len_day = 60*60*24 # Seconds in a day
    len_hour = 60*60   # Seconds in an hour
    len_minute = 60    # Seconds in a minute
    
    # Here we are getting the latest and the earliest
    # timestamp, so we could calculate the step of the
    # graph. ( So the data will be readable )

    latest = 0
    try:
        earliest = data["items"][0]["timestamp"]
    except:
        earliest = 0

    for i in data["items"]:
        if i.get("timestamp", 0) > latest:
            latest = i.get("timestamp", 0)
        if i.get("timestamp", 0) < earliest:
            earliest = i.get("timestamp", 0)

    # Now let's look at our zoom value

    to_zoom = data["zoom"][0] != earliest or data["zoom"][1] != latest

    if data["zoom"][0] < earliest or data["zoom"][0] == 0:
        data["zoom"][0] = earliest
    if data["zoom"][1] > latest or data["zoom"][1] == 0:
        data["zoom"][1] = latest
    
    earliest, latest = data["zoom"]

    # Now I want to make a scale of dates from left
    # to right.

    main_layer.select_font_face("Monospace")
    main_layer.set_font_size(10)
    
    full_date = "%Y-%m-%d %H:%M:%S"
    only_date = "%Y-%m-%d"

    if latest - earliest > 10 * len_day:
        show_format = only_date
        count = int( w / (len("xxxx-xx-xx")*6+12) )
    else:
        show_format = full_date
        count = int( w / (len("xxxx-xx-xx xx:xx:xx")*6+12) )


    # Now I want to show the current date / time for
    # the area where the user is hovering.

    suglen = len("xxxx-xx-xx xx:xx:xx")*6+12
    
    thexm = mx-suglen/2
    if thexm < 2:
        thexm = 2
    elif thexm > w - suglen - 2:
        thexm = w - suglen - 2

    try:
        res_date = int( ( latest - earliest ) / w * mx + earliest )
        show_date = time.strftime(full_date, time.gmtime(res_date))
    except:
        show_date = "0000-00-00"
    
    main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
    main_layer.rectangle(2+thexm,2,len(show_date)*6+2, 14)
    main_layer.fill()

    main_layer.move_to(3+thexm,12)
    main_layer.set_source_rgba(1,1,1,1)

    main_layer.show_text(str(show_date))

    
    # main_layer.set_source_rgba(0.7,0.7,0.7,1)
    # main_layer.move_to( mx, 20 )
    # main_layer.line_to( mx, h )
    # main_layer.stroke()

    # main_layer.set_dash([10,10])
    # main_layer.set_source_rgba(0.2,0.2,0.2,1)
    # main_layer.move_to( mx, 20 )
    # main_layer.line_to( mx, h )
    # main_layer.stroke()
    # main_layer.set_dash([1])

    # And the rest of the dates
        
    for date in range(count):

        try:
            res_date = int( ( latest - earliest ) / count * date + earliest )
            show_date = time.strftime(show_format, time.gmtime(res_date))
        except:
            show_date = "0000-00-00"

        thex = w / count * date

        

        # If not in range of the mouse ( so I could show the current day
        # for that specific area ).
        if int(thex) not in range(int(thexm-suglen/2), int(thexm+suglen)):
        
            main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
            main_layer.rectangle(2+thex,2,len(show_date)*6+2, 14)
            main_layer.fill()

            main_layer.move_to(3+thex,12)
            main_layer.set_source_rgba(1,1,1,1)

            main_layer.show_text(str(show_date))


    

            
    # A step is how often will there be a data point
    # of the graph. Step of one minute, means every
    # point on the graph will consist all the data
    # happened in this minute. 

    
    
    step = (latest - earliest) / (w / 2) # A second
    
    
    # Now we need the smallest and biggest value in a
    # given step

    values = []
    times =  []
    
    pstep = earliest
    s = 0
    av = []
    
    for n, i in enumerate(data["items"]):

        if i.get("timestamp", 0) < earliest:
            continue

        if graph_addition == "add":
            s += float(i.get("amount", i.get("value", 0)))
        elif graph_addition == "average":
            av.append( float(i.get("amount", i.get("value", 0))) )
        elif graph_addition == "last":
            s = float(i.get("amount", i.get("value", 0)))

        if i.get("timestamp", 0) > pstep + step-1:

            pstep = i.get("timestamp", n)
            
            if graph_addition == "average":
                try:
                    values.append(sum(av)/len(av))
                except:
                    values.append(0)
            else:
                values.append(s)
            times.append(pstep)
            s = 0
            av = []

        if i.get("timestamp", 0) > latest:
            break


        
        
    
    # Finding the farthest point from the center
    # center being the 0 (zero)
    try:
        biggest = max(values)
        if min(values) * -1 > biggest:
            biggest = min(values) * -1 # Multuply by -1 reverses the - to a +
    except Exception as e:
        biggest = 1

        
    
    # Now let's draw it

    main_layer.set_line_cap(cairo.LineCap.ROUND)
    
    # POSITIVE VALUE

    try:
        toy = ( zero_at ) - ( ( zero_at ) / biggest * values[0] ) *0.9
    except:
        toy = zero_at
    #toy = min(toy, zero_at)

    main_layer.rectangle(0,0,w,zero_at)
    main_layer.clip()
    
    main_layer.move_to(0, toy)

    

    prex = 0
    prey = toy

    toxes = []
    toyes = []
    
    for n, i in enumerate(values):
        
        tox = w / (latest - earliest) * (times[n]-earliest)
        try:
            toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
        except:
            toy = zero_at

        toxes.append(tox)
        toyes.append(toy)
                
        #toy = min(toy, zero_at)

        
        
        main_layer.curve_to(
                            tox - (tox - prex)/2,
                            prey,
                            
                            prex + (tox - prex)/2,
                            toy,

                            tox,
                            toy)

        prex = tox
        prey = toy

    main_layer.line_to( w, zero_at)
    main_layer.line_to( 0, zero_at)
    main_layer.set_source_rgba(0.2,0.8,0.2,0.5)
    main_layer.fill_preserve()
    main_layer.set_source_rgba(0.2,0.8,0.2,1)
    main_layer.stroke()

    # NEGATIVE VALUE

    try:
        toy = ( zero_at ) - ( ( zero_at ) / biggest * values[0] ) *0.9
    except:
        toy = zero_at
    #toy = max(toy, zero_at)
        
    main_layer.reset_clip()
    main_layer.rectangle(0,zero_at,w,h)
    main_layer.clip()

    
    main_layer.move_to(0, toy)

    prex = 0
    prey = toy

    
    for n, i in enumerate(values):
        
        tox = w / (latest - earliest) * (times[n]-earliest)
        try:
            toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
        except:
            toy = zero_at
        #toy = max(toy, zero_at)
        
        main_layer.curve_to(
            
                            tox - (tox - prex)/2,
                            prey,
                            
                            prex + (tox - prex)/2,
                            toy,
                            

                            tox,
                            toy)

        prex = tox
        prey = toy

    main_layer.line_to( w, zero_at)
    main_layer.line_to( 0, zero_at)
    main_layer.set_source_rgba(0.8,0.2,0.2,0.5)
    main_layer.fill_preserve()
    main_layer.set_source_rgba(0.8,0.2,0.2,1)
    main_layer.stroke()

    main_layer.reset_clip()
    


    
    # Reference line

    main_layer.set_source_rgba(0.7,0.7,0.7,1)
    main_layer.move_to( 0, zero_at )
    main_layer.line_to( w, zero_at )
    main_layer.stroke()

    main_layer.set_dash([10,10])
    main_layer.set_source_rgba(0.2,0.2,0.2,1)
    main_layer.move_to( 0, zero_at )
    main_layer.line_to( w, zero_at )
    main_layer.stroke()

    main_layer.set_dash([1])

    # MOUSE OVER SELECTOR

    def closest(l, v):
        distances = []
        for i in l:
            distances.append(max(i-v, v-i))
        try:
            return l[distances.index(min(distances))]
        except:
            return 0


    selectx = closest(toxes, mx)
    if selectx:
        selecty = toyes[toxes.index(selectx)]


        # Litte circle
        
        main_layer.arc(selectx, selecty, 8, 0, math.pi*2)
        
        main_layer.set_source_rgba(0.2,0.8,0.2,1)
        if selecty > zero_at:
            main_layer.set_source_rgba(0.8,0.2,0.2,1)
        main_layer.fill()

        # Line from that circle downwards
        
        main_layer.move_to(selectx, selecty)
        main_layer.line_to(selectx, zero_at)
        main_layer.stroke()

        # Data about this time frame

        to_data = times[toxes.index(selectx)]
        from_data = to_data - step

        try:
            from_data = time.strftime(show_format, time.gmtime(from_data))
        except:
            from_data = "0000-00-00"
        try:
            to_data = time.strftime(show_format, time.gmtime(to_data))
        except:
            to_data = "0000-00-00"

        
            
        # Counting the largest thing
        plist = ["From: "+from_data,
                 "To: "+to_data,
                 "Total: "+currancy+" "+str(round(values[toxes.index(selectx)], 2)) ]
        
        leng = 0
        for thing in plist:
            if len(str(thing))*6+2 > leng:
                leng = len(str(thing))*6+2

        if selectx > w/2:
            recx = selectx - leng - 10
        else:
            recx = selectx + 10

        if selecty + len(plist)*15 > h:
            recy = selecty - len(plist)*15
        else:
            recy = selecty
            
        main_layer.set_source_rgba(0.1,0.1,0.1,0.7)
        main_layer.rectangle(recx, recy, leng, len(plist)*15)
        main_layer.fill()

        for n, thing in enumerate(plist):
            main_layer.move_to(recx+2, recy+12+(15*n))
            main_layer.set_source_rgba(1,1,1,1)
            main_layer.show_text(thing)
    
    # Now let's get the values ( to the side of the graph )

    for i in range(int(h/20)):

        # TODO: This has to be tuned a bit. It's not perfect. But it's
        # very close.
        
        they = i*20+20

        try:
            value_is = round( biggest / zero_at * (zero_at - they), 2)
        except Exception as e:
            print("what", e)
            value_is = 0
        
        show_value = currancy + " " + str(value_is)
        if mx > w / 2:
            main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
            main_layer.rectangle(2, 2+they,len(show_value)*6+4, 14)
            main_layer.fill()
            main_layer.move_to(3,12+they)
            main_layer.set_source_rgba(1,1,1,1)
            main_layer.show_text(show_value)

        else:
            main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
            main_layer.rectangle(w-len(show_value)*6-4, 2+they,len(show_value)*6+4, 14)
            main_layer.fill()
            main_layer.move_to(w-len(show_value)*6-3,12+they)
            main_layer.set_source_rgba(1,1,1,1)
            main_layer.show_text(show_value)


    # Render a little pressed selector

    if "pressed" in data:

        for i in [data["pressed"], mx]:

            main_layer.set_source_rgba(0.7,0.7,0.7,1)
            main_layer.move_to( i, 0 )
            main_layer.line_to( i, h )
            main_layer.stroke()

            main_layer.set_dash([10,10])
            main_layer.set_source_rgba(0.2,0.2,0.2,1)
            main_layer.move_to( i, 0 )
            main_layer.line_to( i, h )
            main_layer.stroke()

            main_layer.set_dash([1])
        
    # Keep redrawing the graph
    d.queue_draw()
    
def graph_button_press(w, e, data, da):

    data["pressed"] = e.x
    
    print(data["zoom"])
    print(e.x, e.y)

def graph_button_release(w, e, data, da):

    if "pressed" in data:
        x = data["pressed"]

        # If there was no motion
        if x-2 < e.x < x+2:
            data["zoom"] = [0,0]

        else:
            w = da.get_allocated_width()
            zoom0 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * min(x, e.x))
            zoom1 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * max(x, e.x))

            data["zoom"] = [zoom0, zoom1]

        print(data["zoom"])
    del data["pressed"]

def graph(win, data, title="", currancy="$", add_now=True, add_value="Same", graph_addition="add"):

    # adding one more data point for "now"
    if add_now:
        try:
            data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
            last = data["items"][-1].copy()
            last["timestamp"] = int(time.time())
            if not add_value == "Same":
                last["amount"] = add_value
            data["items"].append(last)

        except:
            pass

    
    event_box = Gtk.EventBox()
    da = Gtk.DrawingArea()
    da.set_size_request(100,100)
    da.connect("draw", graph_draw, win, data, currancy, graph_addition)
    event_box.connect("button-press-event", graph_button_press, data, da)
    event_box.connect("button-release-event", graph_button_release, data, da)
    event_box.add(da)


    return event_box
