import gi

import config.data as data

gi.require_version('Gtk', '3.0')
import json
import os
import re
import subprocess
import tempfile
import urllib.parse
import urllib.request
from pathlib import Path

import cairo
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

import modules.icons as icons

SAVE_FILE = os.path.expanduser("~/.pins.json")

icon_size = 80
if data.PANEL_THEME == "Panel" and data.BAR_POSITION in ["Left", "Right"] or data.PANEL_POSITION in ["Start", "End"]:
    icon_size = 36

def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface:
    alloc = widget.get_allocation()
    surface = cairo.ImageSurface(cairo.Format.ARGB32, alloc.width, alloc.height)
    cr = cairo.Context(surface)
    cr.set_source_rgba(1, 1, 1, 0)
    cr.rectangle(0, 0, alloc.width, alloc.height)
    cr.fill()
    widget.draw(cr)
    return surface

def open_file(filepath):
    try:
        subprocess.Popen(["xdg-open", filepath])
    except Exception as e:
        print("Error opening file:", e)

def open_url(url):
    try:
        subprocess.Popen(["xdg-open", url])
    except Exception as e:
        print("Error opening URL:", e)

def is_url(text):

    url_pattern = re.compile(
        r'^(https?|ftp)://'
        r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
        r'localhost|'
        r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
        r'(?::\d+)?'
        r'(?:/?|[/?]\S+)$', re.IGNORECASE)
    return bool(url_pattern.match(text))

def get_favicon_url(url):
    """Extract the base domain from a URL and construct a favicon URL."""
    parsed_url = urllib.parse.urlparse(url)
    base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
    return f"{base_url}/favicon.ico"

def download_favicon(url, callback):
    """Download a favicon asynchronously and call the callback with the result."""
    favicon_url = get_favicon_url(url)

    def do_download():
        temp_file = None
        try:
            temp_fd, temp_path = tempfile.mkstemp(suffix='.ico')
            os.close(temp_fd)
            temp_file = temp_path

            urllib.request.urlretrieve(favicon_url, temp_path)

            GLib.idle_add(callback, temp_path)
        except Exception as e:
            print(f"Error downloading favicon: {e}")

            if temp_file and os.path.exists(temp_file):
                try:
                    os.remove(temp_file)
                except:
                    pass
            GLib.idle_add(callback, None)

    GLib.Thread.new("favicon-download", do_download, None)

class FileChangeHandler(FileSystemEventHandler):
    def __init__(self, app):
        self.app = app

    def on_any_event(self, event):
        if event.is_directory:
            return

        for cell in self.app.cells:
            if cell.content_type == 'file' and cell.content:
                try:
                    cell_real = os.path.realpath(cell.content)
                    src_real = os.path.realpath(event.src_path)
                    dest_real = os.path.realpath(getattr(event, 'dest_path', ''))
                    if cell_real == src_real or (dest_real and cell_real == dest_real):
                        GLib.idle_add(self.handle_file_event, cell, event)
                except Exception:
                    pass

    def handle_file_event(self, cell, event):
        if event.event_type == 'deleted':
            cell.clear_cell()
            self.app.save_state()
        elif event.event_type == 'moved':
            if hasattr(event, 'dest_path') and os.path.exists(event.dest_path):
                cell.content = event.dest_path
                cell.update_display()
                self.app.save_state()
                self.app.add_monitor_for_path(os.path.dirname(event.dest_path))

class Cell(Gtk.EventBox):
    def __init__(self, app, content=None, content_type=None):
        super().__init__(name="pin-cell")
        self.app = app
        self.content = content
        self.content_type = content_type
        self.box = Box(name="pin-cell-box", orientation="v", spacing=4)
        self.add(self.box)
        

        self.favicon_temp_path = None

        target_dest = Gtk.TargetEntry.new("text/uri-list", 0, 0)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [target_dest], Gdk.DragAction.COPY)
        self.connect("drag-data-received", self.on_drag_data_received)

        targets = [
            Gtk.TargetEntry.new("text/uri-list", 0, 0),
            Gtk.TargetEntry.new("text/plain", 0, 1)
        ]
        self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY)
        self.connect("drag-data-get", self.on_drag_data_get)

        self.connect("button-press-event", self.on_button_press)

        self.connect("drag-begin", self.on_drag_begin)

        self.update_display()

    def update_display(self):

        if self.favicon_temp_path and os.path.exists(self.favicon_temp_path):
            try:
                os.remove(self.favicon_temp_path)
                self.favicon_temp_path = None
            except Exception as e:
                print(f"Error removing temp favicon: {e}")
        
        for child in self.box.get_children():
            self.box.remove(child)
        
        if self.content is None:
            label = Label(name="pin-add", markup=icons.paperclip)
            self.box.pack_start(label, True, True, 0)
        else:
            if self.content_type == 'file':
                widget = self.get_file_preview(self.content)
                self.box.pack_start(widget, True, True, 0)
                label = Label(name="pin-file", label=os.path.basename(self.content), justification="center", ellipsization="middle")
                self.box.pack_start(label, False, False, 0)
            elif self.content_type == 'text':
                if is_url(self.content):

                    icon_container = Box(name="pin-icon-container", orientation="v")
                    self.box.pack_start(icon_container, True, True, 0)
                    

                    url_icon = Label(name="pin-url-icon", markup=icons.world, style=f"font-size: {icon_size}px;")
                    icon_container.pack_start(url_icon, True, True, 0)
                    

                    domain = re.sub(r'^https?://', '', self.content)
                    domain = domain.split('/')[0]
                    label = Label(name="pin-url", label=domain, justification="center", ellipsization="end")
                    self.box.pack_start(label, False, False, 0)
                    

                    download_favicon(
                        self.content, 
                        lambda path: self.update_favicon(icon_container, url_icon, path)
                    )
                else:

                    label = Label(name="pin-text", label=self.content.split('\n')[0], justification="center", ellipsization="end", line_wrap="word-char")
                    self.box.pack_start(label, True, True, 0)
        self.box.show_all()
        if not self.app.loading_state:
            self.app.save_state()
    
    def update_favicon(self, container, icon_widget, favicon_path):
        """Update the icon with the downloaded favicon or keep the default."""
        if not favicon_path or not os.path.exists(favicon_path):

            return
        
        try:

            self.favicon_temp_path = favicon_path
            

            if data.PANEL_THEME == "Panel" and data.BAR_POSITION in ["Left", "Right"]:

                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
                    favicon_path, width=36, height=36, preserve_aspect_ratio=True)
            else:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
                    favicon_path, width=48, height=48, preserve_aspect_ratio=True)
            

            container.remove(icon_widget)
            

            img = Gtk.Image.new_from_pixbuf(pixbuf)
            img.set_name("pin-favicon")
            container.pack_start(img, True, True, 0)
            

            container.show_all()
        except Exception as e:
            print(f"Error setting favicon: {e}")


    def get_file_preview(self, filepath):
        try:
            file = Gio.File.new_for_path(filepath)
            info = file.query_info("standard::content-type", Gio.FileQueryInfoFlags.NONE, None)
            content_type = info.get_content_type()
        except Exception:
            content_type = None

        icon_theme = Gtk.IconTheme.get_default()

        if content_type == "inode/directory":
            try:
                pixbuf = icon_theme.load_icon("default-folder", icon_size, 0)
                return Gtk.Image.new_from_pixbuf(pixbuf)
            except Exception:
                print("Error loading folder icon")
                return Gtk.Image.new_from_icon_name("default-folder", Gtk.IconSize.DIALOG)
        
        if content_type and content_type.startswith("image/"):
            try:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
                    filepath, width=icon_size, height=icon_size, preserve_aspect_ratio=True)
                return Gtk.Image.new_from_pixbuf(pixbuf)
            except Exception as e:
                print("Error loading image preview:", e)
        
        elif content_type and content_type.startswith("video/"):
            try:
                pixbuf = icon_theme.load_icon("video-x-generic", icon_size, 0)
                return Gtk.Image.new_from_pixbuf(pixbuf)
            except Exception:
                print("Error loading video icon")
                return Gtk.Image.new_from_icon_name("video-x-generic", Gtk.IconSize.DIALOG)
        else:
            icon_name = "text-x-generic"
            if content_type:
                themed_icon = Gio.content_type_get_icon(content_type)
                if hasattr(themed_icon, 'get_names'):
                    names = themed_icon.get_names()
                    if names:
                        icon_name = names[0]
            try:
                pixbuf = icon_theme.load_icon(icon_name, icon_size, 0)
                return Gtk.Image.new_from_pixbuf(pixbuf)
            except Exception:
                print("Error loading icon", icon_name)
                return Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)

    def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
        if self.content is None and data.get_length() >= 0:
            uris = data.get_uris()
            if uris:
                try:
                    filepath, _ = GLib.filename_from_uri(uris[0])
                    self.content = filepath
                    self.content_type = 'file'
                    self.update_display()
                except Exception as e:
                    print("Error getting file from URI:", e)
        drag_context.finish(True, False, time)

    def on_drag_data_get(self, widget, drag_context, data, info, time):
        if self.content is None:
            return
        if info == 0 and self.content_type == 'file':
            uri = GLib.filename_to_uri(self.content)
            data.set_uris([uri])
        elif info == 1 and self.content_type == 'text':
            data.set_text(self.content, -1)

    def on_drag_begin(self, widget, context):

        if self.content_type == 'file':
            surface = createSurfaceFromWidget(self)
            Gtk.drag_set_icon_surface(context, surface)

    def on_button_press(self, widget, event):
        if self.content is None:
            if event.button == 1:
                self.select_file()
            elif event.button == 2:
                clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
                text = clipboard.wait_for_text()
                if text:
                    self.content = text
                    self.content_type = 'text'
                    self.update_display()
        else:
            if self.content_type == 'file':
                if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
                    open_file(self.content)
                elif event.button == 3:
                    self.clear_cell()
            elif self.content_type == 'text':
                if event.button == 1:

                    if is_url(self.content):

                        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
                        clipboard.set_text(self.content, -1)
                        

                        if not (event.state & Gdk.ModifierType.CONTROL_MASK):
                            open_url(self.content)
                    else:

                        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
                        clipboard.set_text(self.content, -1)
                elif event.button == 3:
                    self.clear_cell()
        return True

    def select_file(self):
        dialog = Gtk.FileChooserDialog(
            title="Select File",
            parent=self.get_toplevel(),
            action=Gtk.FileChooserAction.OPEN
        )
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                           Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
        if dialog.run() == Gtk.ResponseType.OK:
            filepath = dialog.get_filename()
            self.content = filepath
            self.content_type = 'file'
            self.update_display()
        dialog.destroy()

    def clear_cell(self):

        if self.favicon_temp_path and os.path.exists(self.favicon_temp_path):
            try:
                os.remove(self.favicon_temp_path)
                self.favicon_temp_path = None
            except Exception as e:
                print(f"Error removing temp favicon: {e}")
        
        self.content = None
        self.content_type = None
        self.update_display()

class Pins(Gtk.Box):
    def __init__(self, **kwargs):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0)

        self.loading_state = True
        self.monitored_paths = set()
        self.observer = Observer()
        self.event_handler = FileChangeHandler(self)

        self.cells = []


        grid = Gtk.Grid(row_spacing=8, column_spacing=8, name="pin-grid")
        grid.set_column_homogeneous(True)
        grid.set_row_homogeneous(True)








        scrolled_window = ScrolledWindow(child=grid, name="scrolled-window", style_classes="pins", propagate_width=False, propagate_height=False)
        scrolled_window.set_hexpand(True)
        scrolled_window.set_vexpand(True)
        scrolled_window.set_halign(Gtk.Align.FILL)
        scrolled_window.set_valign(Gtk.Align.FILL)
        self.pack_start(scrolled_window, True, True, 0)


        if data.PANEL_THEME == "Panel" and (data.BAR_POSITION in ["Left", "Right"] or data.PANEL_POSITION in ["Start", "End"]):
            for row in range(10):
                for col in range(3):
                    cell = Cell(self)
                    self.cells.append(cell)
                    grid.attach(cell, col, row, 1, 1)
        else:
            for row in range(6):
                for col in range(5):
                    cell = Cell(self)
                    self.cells.append(cell)
                    grid.attach(cell, col, row, 1, 1)

        self.load_state()
        self.loading_state = False
        self.start_file_monitoring()

        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.connect("drag-data-received", self.on_drag_data_received)

    def start_file_monitoring(self):
        for cell in self.cells:
            if cell.content_type == 'file' and cell.content:
                dir_path = os.path.dirname(cell.content)
                if os.path.exists(dir_path) and dir_path not in self.monitored_paths:
                    self.observer.schedule(self.event_handler, dir_path, recursive=False)
                    self.monitored_paths.add(dir_path)
        self.observer.start()

    def add_monitor_for_path(self, path):
        if path not in self.monitored_paths and os.path.exists(path):
            self.observer.schedule(self.event_handler, path, recursive=False)
            self.monitored_paths.add(path)

    def save_state(self):
        state = []
        for cell in self.cells:
            state.append({
                'content_type': cell.content_type,
                'content': cell.content
            })
        try:
            with open(SAVE_FILE, 'w') as f:
                json.dump(state, f)
        except Exception as e:
            print("Error saving state:", e)

    def load_state(self):
        if not os.path.exists(SAVE_FILE):
            return
        try:
            with open(SAVE_FILE, 'r') as f:
                state = json.load(f)
            for i, cell_data in enumerate(state):
                if i < len(self.cells):
                    content = cell_data.get('content')
                    content_type = cell_data.get('content_type')
                    self.cells[i].content = content
                    self.cells[i].content_type = content_type
                    self.cells[i].update_display()
        except Exception as e:
            print("Error loading state:", e)

    def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
        if data.get_length() >= 0:
            uris = data.get_uris()
            for uri in uris:
                try:
                    filepath, _ = GLib.filename_from_uri(uri)
                    for cell in self.cells:
                        if cell.content is None:
                            cell.content = filepath
                            cell.content_type = 'file'
                            cell.update_display()
                            break
                except Exception as e:
                    print("Error getting file from URI:", e)
        drag_context.finish(True, False, time)

    def stop_monitoring(self):

        for cell in self.cells:
            if hasattr(cell, 'favicon_temp_path') and cell.favicon_temp_path and os.path.exists(cell.favicon_temp_path):
                try:
                    os.remove(cell.favicon_temp_path)
                except Exception:
                    pass
                    
        self.observer.stop()
        self.observer.join()
