import tkinter as tk
from tkinter import messagebox
from tkinter import scrolledtext
from tkinter import filedialog
from tkinter import simpledialog
import socket
import threading
import sys
import subprocess
import os
import webbrowser

type_map = {
   '0': 'TXT',
   '1': 'DIR',
   '3': 'ERR',
   '5': 'BIN',
   '7': ' ? ',
   '9': 'BIN',
   'g': 'GIF',
   'h': 'HTM',
   'i': '   ',
   'I': 'IMG',
}

history = []
history_pointer = -1
working_directory = os.path.dirname(__file__)

class ReturnThread(threading.Thread):
   def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self._result = None

   def run(self):
       self._result = self._target(*self._args, **self._kwargs)

   def get_result(self):
       return self._result

def parse_gophermap(response):
   text_field.delete(1.0, tk.END)  # Clear previous text
   for line in response:
       if line:
           item_type = line[0]
           if item_type in type_map:
               try:
                   description, path, server, port = line.split('\t')
               except ValueError as e:
                   print(f"Error:{e}\n{line}")
                   # comment lines are often malformed and contain only a descripton
                   # only abort processing the current line if it is not a comment
                   if item_type == 'i':
                       description = line
                   else:
                       continue

               # clear textfield
               text_field.insert(tk.END, f"{type_map[item_type]} ")
               # item type i is a comment and is not clickable
               if item_type in ['i', '3']:
                   text_field.insert(tk.END, description[1:])
               # only pass the path which should be in this format: URL:http://...
               elif item_type == 'h':
                   text_field.insert(tk.END, description[1:], ("link", path))
               else:
                   link_data = f"{server}/{item_type}/{path}"
                   # add non standard port
                   if int(port) != 70: link_data += f":{port}"
                   text_field.insert(tk.END, description[1:], ("link", link_data))
               text_field.insert(tk.END, "\n")


def bookmarks_list():
   text_field.config(state="normal")
   with open(os.path.join(working_directory, "bookmarks"), 'r') as file:
       content = file.read()
   parse_gophermap(content.split("\n"))
   text_field.config(state="disabled")

def tcp_request(user_input, server, socket,
               port, descriptor, path, query, update_history=True, quirk=False):
   try:
       with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
           s.settimeout(10)
           s.connect((server, port))
           if query:
               if quirk: s.sendall(f"/{'/'.join(path)}\t{query}\n".encode())
               else:     s.sendall(f"{'/'.join(path)}\t{query}\n".encode())
           else:
               if quirk: s.sendall(f"/{'/'.join(path)}\n".encode())
               else:     s.sendall(f"{'/'.join(path)}\n".encode())

           # Receive binary response
           response = b""
           while True:
               # Receive data in chunks
               chunk = s.recv(1024*4)
               if not chunk: break
               response += chunk

       # Some goofball complains that the selector does not start
       # with a "/" while some other nerd complained that sending a
       # "/" first is not compliant client behavoiur.  So as
       # workaround I resend the request with a prepended slash ONCE
       # if the reponse starts with an Error code (3).
       if str(response)[2] == '3' and not quirk:
           print("FIRE")
           return tcp_request(user_input, server, socket,
                       port, descriptor, path, query, update_history, quirk=True)
       else:
           return response, user_input, descriptor, path, update_history

   except Exception as e:
       messagebox.showerror("Error", f"An error occurred: {e}\n")
       print(f"An error occurred: {e}\n")

def handle_response(response, descriptor, path):
   # text file
   if descriptor == '0':
       # strip line feeds
       response = response.decode().split('\n')
       response = [line.rstrip() for line in response]
       response = '\n'.join(response)
       text_field.delete(1.0, tk.END)
       text_field.insert(tk.END, f"{response}\n")
   # directory
   elif descriptor in ['1', '7']: # I assume the query response is always a gophermap?
       # strip line feeds
       response = response.decode().split('\n')
       response = [line.rstrip() for line in response]
       parse_gophermap(response)
   # binary file
   elif descriptor in ['5','9']:
       save_binary_file(response, path[-1])
   # image file
   elif descriptor in ['g','I']:
       open_image_file(response, path[-1])
   else:
       print(f"unknown descriptor: {descriptor}")


# Function to start the TCP request in a new thread
def start_request(update_history=True):
   user_input = entry.get()

   # remove protocol string
   if user_input.startswith("gopher://"):
       user_input = user_input[9:]

   if user_input.startswith("URL:"):
       if messagebox.askyesno("Confirm", "Do you really want to leave gopherspace and open the URL?"):
           # the open_link method removes double slashes. Ugly but it is what it is.
           webbrowser.open(user_input.split(':', 1)[1].replace("/", "//", 1))
       return

   # extract port number if any
   port       = 70 # default
   try:
       port = int(user_input.split(':')[1])
       user_input = user_input.split(':')[0]
   except Exception as e:
       # use default port
       pass

   print(f"port\t{port}")
   print(f"input\t{user_input}")

   descriptor = '1' # default: root gophermap
   path       = ""  # default path
   query      = ""

   # extract server, descriptor and path
   parts      = user_input.split('/')
   # handle trailing slash on root domain
   parts      = [item for item in parts if item.strip()]
   # extract request data
   if len(parts) == 1:
       server = parts[0]
   else:
       server, descriptor, *path = parts

   if descriptor == '7':
       query = simpledialog.askstring("Input", "Please input a query string.")

   global thread
   root.after(100, lambda: thread_callback())
   thread = thread = ReturnThread(target=tcp_request,
                             args=(user_input, server, socket, port,
                                   descriptor, path, query, update_history,))
   thread.start()

def thread_callback():
   if entry.cget("bg") == 'white': entry.config(bg='lightgray')
   else : entry.config(bg='white')

   # rapidly change the color of the uri field to indicate thread acitvity
   if thread and thread.is_alive():
       root.after(200, lambda: thread_callback())

   # handle the response from the tcp thread
   else:
       response, user_input, descriptor, path, update_history = thread.get_result()

       # reset color of entry field after receiving response
       entry.config(bg='white')

       # change state of text field so it can be written to
       text_field.config(state="normal")
       handle_response(response, descriptor, path)
       text_field.config(state="disabled")

       # update history
       global history_pointer
       global history
       # don't update history if the address does not change
       if history and history[-1] == user_input: update_history = False
       # don't update history for images/binary files
       if descriptor in ['g','I','9']: update_history = False
       if update_history:
           # cut off history chain if pointer points not to the last element
           if history_pointer != len(history) - 1:
               history = history[:history_pointer+1]
           history.append(entry.get())
           history_pointer += 1

def enter_link(event):
   event.widget.config(cursor="hand2")

def leave_link(event):
   event.widget.config(cursor="")

def open_link(event):
   link_data   = event.widget.tag_names(f"@{event.x},{event.y}")[1]
   entry.delete(0, tk.END)
   entry.insert(tk.END, link_data.replace("//", "/"))
   start_request()

def history_back():
   global history_pointer
   if history_pointer <= 0: return
   history_pointer = history_pointer - 1
   entry.delete(0, tk.END)
   entry.insert(tk.END, history[history_pointer])
   start_request(False)

def history_forward():
   global history_pointer
   if history_pointer + 1 >= len(history): return
   history_pointer = history_pointer + 1
   entry.delete(0, tk.END)
   entry.insert(tk.END, history[history_pointer])
   start_request(False)

def save_binary_file(binary_data, filename):
   # Open a file save dialog
   file_path = filedialog.asksaveasfilename(
       initialfile=filename,
       filetypes=[("All files", "*.*")],
       title="Save File")

   if file_path:  # If the user didn't cancel the dialog
       try:
           with open(file_path, 'wb') as file:  # Open file in binary write mode
               file.write(binary_data)  # Write binary data to the file

           messagebox.showinfo("Success", "File saved successfully!")
       except Exception as e:
           messagebox.showerror("Error", f"Failed to save file: {e}")

def open_image_file(binary_data, filename):
   try:
       with open(f"/tmp/{filename}", 'wb') as file:  # Open file in binary write mode
           file.write(binary_data)  # Write binary data to the file

       subprocess.run(["xdg-open", f"/tmp/{filename}"])
   except Exception as e:
       messagebox.showerror("Error", f"Failed to open file: {e}")

# TKINTER GUI
root = tk.Tk()
root.title("TKGopher")
root.geometry("800x600")

# ENTRY FIELD
entry = tk.Entry(root)
entry.grid(row=0, column=0, sticky="ew", ipadx=10)
entry.bind('<Return>', lambda event: start_request())  # Call tcp_request on Enter key
entry.focus_set()  # Autofocus the entry field on startup

# BUTTON FRAME
button_frame = tk.Frame(root)
button_frame.grid(row=0, column=1)
button = tk.Button(button_frame, text="Go", command=start_request)
button.pack(side=tk.LEFT, pady=5)
button = tk.Button(button_frame, text="<<", command=history_back)
button.pack(side=tk.LEFT, pady=5)
button = tk.Button(button_frame, text=">>", command=history_forward)
button.pack(side=tk.LEFT, pady=5)
button = tk.Button(button_frame, text="Bookmarks", command=bookmarks_list)
button.pack(side=tk.LEFT, pady=5)

# SCROLLED TEXT FIELD
text_field = scrolledtext.ScrolledText(root, wrap=tk.WORD, width=35, height=10)
text_field.grid(row=1, column=0, columnspan=2, sticky="nsew")

# Configure the "link" tag to make it clickable
text_field.tag_config("link", foreground="blue", underline=1)
text_field.tag_bind("link", "<Button-1>", open_link)
text_field.tag_bind("link", "<Enter>", enter_link)
text_field.tag_bind("link", "<Leave>", leave_link)
# disable the text field
text_field.config(state="disabled")

# grid configuration
root.grid_columnconfigure(0, weight=1, minsize=100)
root.grid_columnconfigure(1, minsize=150)
root.grid_rowconfigure(1, weight=1)

# Function to exit the application
def exit_app(event=None):
   root.quit()

def mouse_button_event(event):
   if event.num == 8:
       history_back()
   elif event.num == 9:
       history_forward()
   else:
       # print(f"Button {event.num} pressed, but no action defined.")
       return

# Keyboard and Mouse bindings
root.bind('<Control-q>', exit_app)
root.bind("<Button>", mouse_button_event)
root.bind("<Button>", mouse_button_event)
root.bind("<space>", lambda event: text_field.yview_scroll(1, "pages"))

# Startup
if len(sys.argv) > 1:
   entry.delete(0, tk.END)
   entry.insert(0, sys.argv[1])
   root.after(100, lambda: start_request())
else:
   root.after(10, lambda: bookmarks_list())

# Start the GUI event loop
root.mainloop()