#!/usr/bin/env python3

import tkinter as tk
import tkinter.simpledialog as dialog
from tkinter.filedialog import asksaveasfilename as savedialog
from connect import connect as conn
from parser import parser
import time
import sys
import json
import os.path
from io import BytesIO
from PIL import Image, ImageTk
import webbrowser as wb

class GUI:
    def __init__(self):
        self.history = []
        self.history_location = -1
        self.message_bar_content = ''
        self.read_config()
        self.conn = conn()
        self.parser = parser()
        self.search = None

        #colors
        self.FG = '#E0E2E4'
        self.BG = '#2F393C'
        self.LINK = '#E8E2B7'
        self.FLINK = '#93C763'
        self.ACTIVELINK = '#678CB1'
        self.HLB = '#804000'
        self.HLF = self.FG
        self.STATUS_BG = '#293134'
        self.STATUS_FG = '#FFCD22'
        self.ERROR = '#EC7600'
        self.BAR_BG = self.STATUS_BG
        self.BAR_FG = self.BG
        self.BAR_HLB = self.HLB
        self.BAR_HLF = self.FG
        self.BAR_SLOT = self.FG
        self.SCROLL = '#434A57'
        self.TYPES = '#A082BD'
        self.MENU_BG = self.BAR_BG
        self.MENU_FG = self.FG
        self.MENU_HLB = self.LINK
        self.MENU_HLF = self.BAR_BG

        #configure root window
        self.root = tk.Tk(className='Burrow')
        self.root.title('Burrow')
        sh = self.root.winfo_screenheight()
        sw = self.root.winfo_screenwidth()
        w = int(sw * 0.7)
        h = sh - 200
        self.root.geometry("{}x{}+{}+{}".format(w, h, sw//2-w//2, 50))
        self.add_assets()

        #main frame objects
        self.top_bar = tk.Frame(self.root, padx=10, height=50, relief=tk.FLAT, bd=2, bg=self.BAR_BG)
        self.body = tk.Frame(self.root, relief=tk.FLAT, bd=0, bg=self.BG)
        self.status_bar = tk.Frame(self.root, height="20", relief=tk.FLAT, bg=self.STATUS_BG, takefocus=0)

        #top bar objects
        self.btn_back = tk.Button(self.top_bar, image=self.img_back, bd=0, highlightthickness=0, takefocus=1, bg=self.BAR_BG)
        self.btn_forward = tk.Button(self.top_bar,image=self.img_forward, bd=0, highlightthickness=0, bg=self.BAR_BG)
        self.btn_favorite = tk.Button(self.top_bar,image=self.img_favorite, bd=0, highlightthickness=0, bg=self.BAR_BG)
        self.btn_home = tk.Button(self.top_bar, image=self.img_home, bd=0, highlightthickness=0, bg=self.BAR_BG)
        self.entry_url = tk.Entry(self.top_bar, selectbackground=self.HLB, selectforeground=self.HLF, highlightcolor=self.FG, highlightbackground=self.BAR_BG,  fg=self.BAR_FG, bg=self.BAR_SLOT)
        self.btn_menu = tk.Button(self.top_bar, image=self.img_menu, bd=0, highlightthickness=0, bg=self.BAR_BG)

        #body objects
        self.scroll_bar = tk.Scrollbar(self.body, bg=self.BAR_BG, bd=0, highlightthickness=0, troughcolor=self.BG, activebackground=self.SCROLL, activerelief=tk.RAISED)
        self.site_display = tk.Text(self.body, bg=self.BG, foreground=self.FG, padx=20, pady=20, wrap=tk.WORD, state=tk.DISABLED, spacing2=2, spacing1=2, spacing3=2,  yscrollcommand=self.scroll_bar.set, highlightcolor=self.BG, highlightbackground=self.BAR_BG, relief=tk.FLAT, font="TkFixedFont")
        self.scroll_bar.config(command=self.site_display.yview, width=20, relief=tk.RIDGE)
        self.site_display.tag_configure('linkcolor', foreground=self.LINK, spacing1=5, spacing2=5, spacing3=5)
        self.site_display.tag_configure('favoritecolor', foreground=self.FLINK, spacing1=5, spacing2=5, spacing3=5)

        self.site_display.tag_configure('type_tag', background=self.BG, foreground=self.TYPES, spacing2=1, spacing1=1, spacing3=1)
        self.site_display.tag_configure('error_text', foreground=self.ERROR, spacing1=5, spacing2=5, spacing3=5)

        #status bar objects
        self.status_info = tk.Label(self.status_bar, textvariable=self.message_bar_content, bg=self.STATUS_BG, takefocus=0, fg=self.ACTIVELINK)

        #menu objects
        self.context_menu = tk.Menu(self.body, tearoff=0, bg=self.MENU_BG, fg=self.MENU_FG, activebackground=self.MENU_HLB, activeforeground=self.MENU_HLF, activeborderwidth=0)

        self.pack_geometry()
        self.add_status_titles()
        self.add_event_listeners()

        #load the home screen
        self.load_home_screen(1)


    #-----------Start GUI configuration-----------------------


    def add_event_listeners(self):
        buttons = [
            self.btn_back,
            self.btn_forward,
            self.btn_favorite,
            self.btn_home,
            self.btn_menu
        ]

        for x in buttons:
            x.bind('<Enter>', self.update_status)
            x.bind('<Leave>', self.clear_status)
            x.config(activebackground=self.BG)
        self.entry_url.bind('<Return>', self.handle_request)
        self.btn_back.bind('<ButtonRelease-1>', self.go_back)
        self.btn_forward.bind('<ButtonRelease-1>', self.go_forward)
        self.btn_home.bind('<ButtonRelease-1>', self.load_home_screen)
        self.site_display.bind("<Up>", lambda event: self.site_display.yview_scroll(-1, 'units'))
        self.site_display.bind("<Down>", lambda event: self.site_display.yview_scroll(1, 'units'))
        self.site_display.bind("<Button-1>", lambda event: self.site_display.focus_set())
        self.site_display.bind("k", lambda event: self.site_display.yview_scroll(-1, 'units'))
        self.site_display.bind("j", lambda event: self.site_display.yview_scroll(1, 'units'))
        self.site_display.bind("h", self.go_back)
        self.site_display.bind("l", self.go_forward)
        self.entry_url.bind("<Button-1>", lambda event: self.entry_url.focus_set())
        self.root.protocol('WM_DELETE_WINDOW', self.close_window)
        self.btn_favorite.bind("<Button-1>", self.add_to_favorites)
        self.site_display.tag_bind('generic_r_click', "<Button-3>", (lambda event, href=None: self.show_context_menu(event, href)))


    def pack_geometry(self):
        self.top_bar.pack(expand=False,fill=tk.BOTH,side=tk.TOP,anchor=tk.NW)
        self.top_bar.pack_propagate(False)
        self.body.pack(expand=True,fill=tk.BOTH,side=tk.TOP)
        self.status_bar.pack(expand=False,fill=tk.X,side=tk.TOP,anchor=tk.SW)
        self.btn_back.pack(side=tk.LEFT, padx=(0,20))
        self.btn_forward.pack(side=tk.LEFT, padx=(0,20))
        self.btn_home.pack(side=tk.LEFT, padx=(0,20))
        self.entry_url.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=5, ipadx=10)
        self.btn_favorite.pack(side=tk.LEFT, padx=(10,10))
        self.btn_menu.pack(side=tk.LEFT)
        self.scroll_bar.pack(side=tk.RIGHT,fill=tk.Y)
        self.site_display.pack(expand=True, side=tk.TOP, fill=tk.BOTH)
        self.status_info.pack(side=tk.LEFT)


    def add_status_titles(self):
        self.btn_back.pop_title = 'Back'
        self.btn_forward.pop_title = 'Forward'
        self.btn_favorite.pop_title = 'Favorite'
        self.btn_home.pop_title = 'Home'
        self.btn_menu.pop_title = 'Menu'


    def add_assets(self):
        back = Image.open('./images/back.png')
        self.img_back = ImageTk.PhotoImage(back)
        forward = Image.open('./images/forward.png')
        self.img_forward = ImageTk.PhotoImage(forward)
        favorite = Image.open('./images/favorite.png')
        self.img_favorite = ImageTk.PhotoImage(favorite)
        home = Image.open('./images/home.png')
        self.img_home = ImageTk.PhotoImage(home)
        settings = Image.open('./images/settings.png')
        self.img_menu = ImageTk.PhotoImage(settings)
        self.message_bar_content = tk.StringVar()
        self.message_bar_content.set('Ready.')


    def show_context_menu(self, e, href=None):
        current_page = self.entry_url.get()
        self.context_menu.delete(0,20)
        #add navigation options
        if len(self.history) > 1 and self.history_location > 0:
            back = (lambda event=e: self.go_back(event))
            self.context_menu.add_command(label=" Back ", command=back)
        else:
            self.context_menu.add_command(label=" Back ", state=tk.DISABLED)
        if len(self.history) > 1 and self.history_location < len(self.history) - 1:
            forward = (lambda event=e: self.go_forward(event))
            self.context_menu.add_command(label=" Forward ", command=forward)
        else:
            self.context_menu.add_command(label=" Forward ", state=tk.DISABLED)
        refresh = (lambda event=e, link=current_page: self.handle_request(event, link, False))
        self.context_menu.add_command(label=" Refresh ", command=refresh)
        if current_page != 'home':
            home = (lambda event=e: self.load_home_screen(event))
            self.context_menu.add_command(label=" Home ", command=home)
        else:
            self.context_menu.add_command(label=" Home ", state=tk.DISABLED)
        save_as_file = (lambda data=self.site_display.get(1.0,tk.END), url=current_page: self.write_to_file(data, url))
        self.context_menu.add_command(label=" Save as... ", command=save_as_file)

        if href:
            self.context_menu.add_separator()
            copy_link = (lambda link=href: self.copy_to_clipboard(link))
            self.context_menu.add_command(label=" Copy URL to clipboard ", command=copy_link)

            if self.is_favorite(href):
                self.context_menu.add_separator()
                delete_favorite = (lambda event=e, link=href: self.remove_favorite(event, link))
                self.context_menu.add_command(label=" Delete from favorites ", command=delete_favorite)
                rename_favorite = (lambda event=e, link=href: self.rename_favorite(event, link))
                self.context_menu.add_command(label=" Rename this favorite ", command=rename_favorite)
            elif href:
                self.context_menu.add_separator()
                add_favorite = (lambda event=e, link=href: self.add_to_favorites(event, link))
                self.context_menu.add_command(label=" Add to favorites ", command=add_favorite)
        self.context_menu.tk_popup(e.x_root, e.y_root)


    def copy_to_clipboard(self, text):
        self.root.clipboard_clear()
        self.root.clipboard_append(text)


    # ------------Start navigation methods---------------------------


    def handle_request(self,event=False, url=False, history=True):
        self.loading_bar = tk.Label(self.entry_url, text=' Loading... ', width=12, relief=tk.FLAT, height=1, fg='#FFFFFF', bg=self.TYPES)
        self.loading_bar.pack(side=tk.RIGHT, padx=(0,10))
        self.loading_bar.update_idletasks()

        url = url if url else self.entry_url.get()
        parsed_url = self.parse_url(url)

        if not parsed_url:
            if url == 'home':
                return self.load_home_screen(history)
            else:
                data = {'type': '3', 'body': '3ERROR: Improperly formatted URL\tfalse\tnull.host\t1\n'}
                # return False
        elif parsed_url['protocol'] == 'http://':
            wb.open(url,2,True)
            self.populate_url_bar(self.history[-1])
            self.loading_bar.destroy()
            return False

        self.populate_url_bar(url)

        if history:
            self.add_to_history(url)

        if parsed_url and parsed_url['type'] == '7':
            self.show_search()
            return False # display search
        elif not parsed_url:
            data = {'type': '3', 'body': '3ERROR: Improperly formatted URL\tfalse\tnull.host\t1\n'}
        else:
            data = self.execute_address(parsed_url)

        self.send_to_screen(data['body'],data['type'])


    def parse_url(self, url=False):
        parsed_url = self.parser.parse_url(url)

        if not parsed_url:
            return False

        return parsed_url


    def execute_address(self, url):
        response = self.conn.request(url['resource'], url['host'], url['type'], url['port'])

        if not response:
            # send error to screen
            print('ERROR in execute address...')
            return False


        # Get the data to the screen
        self.site_display.focus_set()
        self.config["last_viewed"] = url

        return response


    def add_to_history(self, url):
        self.history = self.history[:self.history_location+1]
        self.history.append(url)
        self.history_location = len(self.history) - 1



    def gotolink(self, event, href, tag_name):
        if href.find('URL:') >= 0:
            href = href.split('URL:')[1]
            wb.open(href, 2, True)
            return True
        element = event.widget
        element.tag_config(tag_name, background=self.ACTIVELINK)
        element.update_idletasks()  # make sure change is visible
        time.sleep(.5)  # optional delay to show changed text
        element.tag_config(tag_name, background=self.BG)  # restore tag text style
        element.update_idletasks()
        self.handle_request(False,href)


    def load_home_screen(self,event=False):
        with open('./home.gopher','r') as f:
            data = f.read()
        self.entry_url.delete(0, tk.END)
        self.entry_url.insert(tk.END, 'home')
        if event:
            self.add_to_history('home')
        data2 = self.load_favorites()
        link_count = self.send_to_screen(data, '1', True)
        self.send_to_screen(data2, '1', False)


    def go_back(self, event):
        if len(self.history) <= 1 or self.history_location <= 0:
            return False

        self.history_location -= 1
        href = self.history[self.history_location]
        self.handle_request(False, href, False)


    def go_forward(self, event):
        if len(self.history) <= 1 or self.history_location >= len(self.history) - 1:
            return False
        self.history_location += 1
        href = self.history[self.history_location]
        self.handle_request(False, href, False)


    #-------------Start view methods----------------


    def load_favorites(self):
        header = ''
        #soon add code to load in favorites here
        for x in self.config["favorites"]:
            url = self.parser.parse_url(x["url"])
            if not url:
                continue
            entry = '{}{}\t{}\t{}\t{}\n'.format(url['type'], x['name'], url['resource'], url['host'], url['port'])
            header += entry
        return header


    def show_search(self):
        text1 = ' __   ___       __   __\n/__` |__   /\  |__) /  ` |__|\n.__/ |___ /~~\ |  \ \__, |  |\n\n\nPlease enter your search terms and press the enter key:\n\n'
        self.search = tk.Entry(width='50')
        self.search.bind('<Return>', self.query_search_engine)
        self.site_display.config(state=tk.NORMAL)
        self.site_display.delete(1.0, tk.END)
        self.site_display.insert(tk.END,text1)
        self.site_display.window_create(tk.END,window=self.search)
        self.site_display.config(state=tk.DISABLED)
        self.search.focus_set()

        try:
            self.loading_bar.destroy()
        except:
            pass



    def query_search_engine(self, event):
        base_url = self.entry_url.get()
        base_url = base_url.replace('/7/','/1/',1)
        query = self.search.get()
        url = '{}\t{}'.format(base_url,query)
        self.populate_url_bar(url)
        self.handle_request(False, url)
        self.search = None


    def show_menu(self, data, clear=True):
        if not data:
            #error handling will go here
            return False

        types = {
                    '0': '( TXT )',
                    '1': '( MNU )',
                    '3': '( ERR )',
                    '7': '( INT )',
                    '9': '( BIN )',
                    'g': '( GIF )',
                    'I': '( IMG )',
                    'h': '( HTM )',
                    'i': '( INF )',
                    's': '( SND )',
                    'p': '( PNG )'
                }

        self.site_display.config(state=tk.NORMAL)

        if clear:
            self.site_display.delete(1.0, tk.END)

        if clear:
            self.link_count = 0

        for x in data:
            if x['type'] == 'i':
                self.site_display.insert(tk.END,'        \t\t{}\n'.format(x['description']), ('generic_r_click'))
            elif x['type'] == '3':
                self.site_display.insert(tk.END,'        \t\t{}\n'.format(x['description']), ('generic_r_click'))
            elif x['type'] in types:
                # adapted from:
                # https://stackoverflow.com/questions/27760561/tkinter-and-hyperlinks
                if x['port'] and x['port'][0] != ':':
                    x['port'] = ':{}'.format(x['port'])

                link = 'gopher://{}{}/{}{}'.format(x['host'], x['port'], x['type'], x['resource'])

                tag_name = 'link{}'.format(self.link_count)
                callback = (lambda event, href=link, tag_name=tag_name: self.gotolink(event, href, tag_name))
                # favorite = [x for x in self.config['favorites'] if x['url'] == link]
                favorite = self.is_favorite(link)
                self.site_display.tag_bind(tag_name, "<Button-1>", callback)
                self.site_display.insert(tk.END, types[x['type']], ('type_tag',))
                self.site_display.insert(tk.END,'\t\t')
                callback_menu = (lambda event, href=link: self.show_context_menu(event, href))
                self.site_display.tag_bind(tag_name, '<Button-3>', callback_menu)

                if favorite:
                    styletag = 'favoritecolor'
                else:
                    styletag = 'linkcolor'

                hover = (lambda event, href=link, tag_name=tag_name: self.hoverlink(event, href, tag_name))
                clear = (lambda event, tag_name=tag_name: self.clear_status(event, tag_name))
                self.site_display.tag_bind(tag_name, "<Enter>", hover)
                self.site_display.tag_bind(tag_name, '<Leave>', clear)

                self.site_display.insert(tk.END, x['description'], (tag_name,styletag))
                self.site_display.insert(tk.END, '\n')
                self.link_count += 1
        self.site_display.config(state=tk.DISABLED)
        return self.link_count


    def show_text(self, data):
        if data[-2:] == '.\n':
            data = data[:-2]
        self.site_display.config(state=tk.NORMAL)
        self.site_display.delete(1.0, tk.END)
        self.site_display.insert(tk.END, data, 'generic_r_click')
        self.site_display.config(state=tk.DISABLED)


    def show_image(self, data):
        self.current_image = self.build_image(data)
        callback = (lambda event, image=data, write='wb': self.write_to_file(contents=image, event=event, write=write))
        hover = (lambda event, href='Download this image...', tag_name='image_download': self.hoverlink(event, href, tag_name))
        clear = (lambda event, tag_name='image_download': self.clear_status(event, tag_name))
        self.site_display.tag_bind('image_download', "<Button-1>", callback)
        self.site_display.tag_bind('image_download', "<Enter>", hover)
        self.site_display.tag_bind('image_download', '<Leave>', clear)
        self.site_display.config(state=tk.NORMAL)
        self.site_display.delete(1.0, tk.END)
        self.site_display.insert(tk.END,'Download this image',('linkcolor','image_download'))
        self.site_display.insert(tk.END, '\n\n')
        self.site_display.image_create(tk.END, image = self.current_image)
        self.site_display.config(state=tk.DISABLED)


    def show_bin_download(self, data):
        url = self.entry_url.get()
        filename = url.rpartition('/')
        if len(filename) > 1:
            filename = filename[-1]
        else:
            filename = ''

        callback = (lambda event, bindata=data, write='wb': self.write_to_file(contents=bindata, event=event, write=write))
        hover = (lambda event, href='Download {}'.format(filename), tag_name='bin_download': self.hoverlink(event, href, tag_name))
        clear = (lambda event, tag_name='bin_download': self.clear_status(event, tag_name))
        self.site_display.tag_bind('bin_download', "<Button-1>", callback)
        self.site_display.tag_bind('bin_download', "<Enter>", hover)
        self.site_display.tag_bind('bin_download', '<Leave>', clear)
        self.site_display.config(state=tk.NORMAL)
        self.site_display.delete(1.0, tk.END)
        self.site_display.insert(tk.END,'Download ')
        self.site_display.insert(tk.END,filename,('linkcolor','bin_download'))
        self.site_display.config(state=tk.DISABLED)


    def send_to_screen(self, data, itemtype='1', clear=True):
        if itemtype in ['0','h']:
            self.show_text(data)
        elif itemtype in ['1','3','7']:
            data = self.parser.parse_menu(data)
            self.show_menu(data, clear)
        elif itemtype in ['p','I','g']:
            self.show_image(data)
        elif itemtype in ['s','9','M','c',';','d','5']:
            self.show_bin_download(data)

        try:
            self.loading_bar.destroy()
        except:
            pass


    def update_status(self, event, href=False):
        if href:
            self.message_bar_content.set(href)
        else:
            self.message_bar_content.set(event.widget.pop_title)


    def clear_status(self, event, tag_name=False):
        if tag_name:
            e = event.widget
            e.tag_config(tag_name, underline=0)
            self.site_display.config(cursor='xterm')
            e.update_idletasks()
        self.message_bar_content.set('')


    def populate_url_bar(self, url):
        self.entry_url.delete(0, tk.END)
        self.entry_url.insert(tk.END, url)


    def hoverlink(self, event, href, tag_name):
        self.update_status(event, href)
        e = event.widget
        e.tag_config(tag_name, underline=1)
        self.site_display.config(cursor="arrow")
        e.update_idletasks()


    def build_image(self, bytes_str):
        stream = BytesIO(bytes_str)
        pilimage = Image.open(stream)
        tkimage = ImageTk.PhotoImage(pilimage)
        return tkimage



    #--------Start file handling methods------------


    def read_config(self, url='./go.config.json'):
        if not os.path.isfile(url):
            self.create_config()
        with open('./go.config.json', 'r') as f:
            config = f.read()
        config = json.loads(config)
        self.config = config


    def write_config(self, config, url='./go.config.json'):
        with open(url, 'w') as f:
            data = json.dumps(config)
            f.write(data)


    def create_config(self):
        config = {"favorites": [],"last_viewed": None}
        self.write_config(config)


    def close_window(self):
        self.write_config(self.config)
        self.root.destroy()

    def is_favorite(self, href):
        for val in self.config['favorites']:
            if val['url'] == href:
                return True
        return False


    def rename_favorite(self, event, link):
        index = None
        for ind, val in enumerate(self.config['favorites']):
            if val['url'] == link:
                index = ind
                break
        if index is not None:
            title = self.config['favorites'][index]['name']
            new_name = dialog.askstring("Rename favorite","\n     Change favorite name to:     \n".format(title), initialvalue=title)
            if new_name:
                self.config['favorites'][index]['name'] = new_name
                self.write_config(self.config)
                if self.entry_url.get() == 'home':
                    self.load_home_screen()
                return True
        return False


    def remove_favorite(self, event, href):
        index = None
        for ind, val in enumerate(self.config['favorites']):
            if val['url'] == href:
                index = ind
                break
        if index is not None:
            self.config['favorites'].pop(index)
            self.write_config(self.config)
        self.load_home_screen()


    def add_to_favorites(self, event, url=None):
        favorite_name = dialog.askstring("Add to favorites", "What would you like to title this favorite?")
        if url is None:
            url = self.entry_url.get()
        if not favorite_name or not url:
            return False
        favorite = {"url": url, "name": favorite_name}
        self.config["favorites"].append(favorite)
        self.write_config(self.config)
        if self.entry_url.get() == 'home':
            self.load_home_screen()
        return True


    def write_to_file(self, contents=None, page_url=None, event=None, write='w'):
        url = self.entry_url.get()
        filetype = url.rpartition('.')
        if len(filetype) > 1:
            filetype = filetype[-1]
        else:
            filetype = 'txt'

        filename = savedialog(initialdir="~/Desktop/", defaultextension='.{}'.format(filetype), title="Save As File", filetypes=((filetype,'*.{}'.format(filetype)),('all files','*.*')))
        if not filename or filename is None or contents is None:
            return False
        with open(filename, write) as f:
            f.write(contents)
        return True



if __name__ == '__main__':
    app = GUI()
    app.root.mainloop()