using Adw;
using Gtk;
using Gee;

public class TranscriptView : Adw.Bin
{
    public MessageListModel? model {
        get {
            return _model;
        }
        set {
            if (_model != null) {
                _model.disconnect(messages_changed_handler_id);
                _model.unwatch_updates();
            }

            _model = value;

            if (value != null) {
                reset_for_conversation_change();

                weak TranscriptView self = this;
                messages_changed_handler_id = value.messages_changed.connect(() => {
                    self.reload_messages();
                });

                value.load_messages();
                value.watch_updates();
            } else {
                transcript_drawing_area.set_messages(new ArrayList<Message>());
            }
        }
    }

    public string title {
        get { return title_label.label; }
        set { title_label.label = value; }
    }

    private MessageListModel? _model = null;
    private Adw.ToolbarView container;
    private Label title_label = new Label("Messages");

    private Overlay overlay = new Overlay();
    private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea();
    private ScrolledWindow scrolled_window = new ScrolledWindow();
    private ulong messages_changed_handler_id = 0;
    private bool needs_reload = false;

    private Graphene.Point _hovering_text_view_origin = Graphene.Point() { x = 0, y = 0 };
    private TextView _hovering_text_view = new TextView();

    private bool _queued_url_open = false;
    private VisibleLayout? hovered_text_bubble = null;
    private VisibleLayout? locked_text_bubble = null;

    public TranscriptView() {
        container = new Adw.ToolbarView();
        set_child(container);
        
        // Set minimum width for the transcript view
        set_size_request(330, -1);

        overlay.set_child(transcript_drawing_area);

        weak TranscriptView self = this;
        overlay.get_child_position.connect((child, out allocation) => {
            allocation = Gtk.Allocation() { x = 0, y = 0, width = 0, height = 0 };
            if (self.hovered_text_bubble != null) {
                var rect = self.hovered_text_bubble.rect;
                allocation.x = (int)(rect.origin.x + self._hovering_text_view_origin.x);
                allocation.y = (int)(rect.origin.y - self._hovering_text_view_origin.y);
                allocation.width = (int)rect.size.width;
                allocation.height = (int)rect.size.height;

                return true;
            }

            return true; 
        });

        scrolled_window.set_child(overlay);
        scrolled_window.add_css_class("flipped-y-axis");
        transcript_drawing_area.viewport = scrolled_window.vadjustment;
        container.set_content(scrolled_window);

        // Connect to the adjustment's value_changed signal
        scrolled_window.vadjustment.value_changed.connect(() => {
            transcript_drawing_area.viewport = scrolled_window.vadjustment;
        });

        var header_bar = new Adw.HeaderBar();
        title_label.single_line_mode = true;
        title_label.ellipsize = Pango.EllipsizeMode.END;
        header_bar.set_title_widget(title_label);
        container.add_top_bar(header_bar);

        // This is an invisible text view that's used to handle selection.
        _hovering_text_view.add_css_class("hovering-text-view");
        _hovering_text_view.add_css_class("flipped-y-axis");
        _hovering_text_view.set_editable(false);
        _hovering_text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR);
        overlay.add_overlay(_hovering_text_view);

        // When the selection changes, lock the text bubble so that if the cursor moves to another bubble we don't clear it. 
        _hovering_text_view.buffer.mark_set.connect((location, end) => {
            if (location.get_offset() == 0) { return; }
            self.on_hovering_text_view_clicked(location, end);
        });

        // When the mouse hovers over a text bubble, configure the hovering text view to show the text of the bubble.
        transcript_drawing_area.on_text_bubble_hover.connect((visible_text_layout) => {
            if (visible_text_layout != null) {
                configure_hovering_text_view(visible_text_layout);
            }
        });

        // This is triggered when another bubble is currently locked, and the user clicks on a new bubble.
        transcript_drawing_area.on_text_bubble_click.connect((visible_text_layout) => {
            if (visible_text_layout != null) {
                locked_text_bubble = null;
                configure_hovering_text_view(visible_text_layout);
            }
        });

        transcript_drawing_area.on_image_bubble_activate.connect((attachment_guid) => {
            self.open_attachment(attachment_guid);
        });

        Repository.get_instance().attachment_downloaded.connect((attachment_guid) => {
            debug("Attachment downloaded: %s", attachment_guid);

            // See if this attachment is part of this transcript. 
            bool contains_attachment = false;
            foreach (var message in _model.messages) {
                foreach (var attachment in message.attachments) {
                    if (attachment.guid == attachment_guid) {
                        contains_attachment = true;
                        break;
                    }
                }
            }

            if (contains_attachment && !needs_reload) {
                debug("Queueing reload of messages for attachment download");

                needs_reload = true;
                GLib.Idle.add(() => {
                    if (needs_reload) {
                        debug("Reloading messages for attachment download");
                        model.load_messages(true);
                        needs_reload = false;
                    }

                    return false;
                }, GLib.Priority.HIGH);
            }
        });
    }

    delegate void OpenPath(string path);
    private void open_attachment(string attachment_guid) {
        OpenPath open_path = (path) => {
            try {
                GLib.AppInfo.launch_default_for_uri("file://" + path, null);
            } catch (GLib.Error e) {
                warning("Failed to open image %s: %s", path, e.message);
            }
        };

        try {
            var attachment_info = Repository.get_instance().get_attachment_info(attachment_guid);
            if (attachment_info.downloaded == true) {
                // We already have it, so open it. 
                open_path(attachment_info.path);
            } else {
                // We need to download this, then open it once the downloaded signal is emitted. 
                Repository.get_instance().download_attachment(attachment_guid, false);

                // TODO: Should probably indicate progress here. 
                
                ulong handler_id = 0;
                handler_id = Repository.get_instance().attachment_downloaded.connect((guid) => {
                    if (guid == attachment_guid) {
                        try {
                            var updated_attachment_info = Repository.get_instance().get_attachment_info(attachment_guid);
                            open_path(updated_attachment_info.path);
                        } catch (GLib.Error e) {
                            warning("Failed to get attachment info after download: %s", e.message);
                        }

                        Repository.get_instance().disconnect(handler_id);
                    }
                });
            }
        } catch (GLib.Error e) {
            warning("Failed to get attachment info: %s", e.message);
        }
    }

    private void on_hovering_text_view_clicked(TextIter location, TextMark end) {
        lock_text_bubble(hovered_text_bubble);

        if (!_queued_url_open) {
            _queued_url_open = true;

            // 100ms timeout to let the selection state settle. (this is a workaround)
            GLib.Timeout.add(100, () => {
                open_url_at_location(location);
                _queued_url_open = false;
                return false;
            }, GLib.Priority.HIGH);
        }
    }

    private void open_url_at_location(TextIter location) {
        Gtk.TextTag? underline_tag = null;
        foreach (unowned Gtk.TextTag tag in location.get_tags()) {
            if (tag.underline != Pango.Underline.NONE) {
                underline_tag = tag;
                break;
            }
        }

        if (underline_tag == null) {
            return; // Click wasn't on an underlined (i.e. link) region
        }

        // Determine the full extent (start/end iters) of this underlined region
        Gtk.TextIter start_iter = location;
        Gtk.TextIter end_iter = location;
        start_iter.backward_to_tag_toggle(underline_tag);
        end_iter.forward_to_tag_toggle(underline_tag);

        string url = _hovering_text_view.buffer.get_text(start_iter, end_iter, false);

        // Try to open the URL – guard against malformed data
        if (url != null && url.strip().length > 0) {
            try {
                GLib.AppInfo.launch_default_for_uri(url.strip(), null);
            } catch (GLib.Error e) {
                warning("Failed to open URL %s: %s", url, e.message);
            }
        }
    }
    

    private void reset_for_conversation_change() {
        locked_text_bubble = null;
        hovered_text_bubble = null;
        _hovering_text_view.buffer.text = "";
        overlay.queue_allocate();

        // Reset scroll position by updating the existing adjustment
        scrolled_window.vadjustment.value = 0;
        scrolled_window.vadjustment.upper = 0;
        scrolled_window.vadjustment.page_size = 0;
    }

    private void lock_text_bubble(VisibleLayout? visible_text_layout) {
        if (visible_text_layout != null) {
            locked_text_bubble = visible_text_layout;
            configure_hovering_text_view(visible_text_layout);
        }
    }

    private void configure_hovering_text_view(VisibleLayout? visible_text_layout) {
        hovered_text_bubble = visible_text_layout;

        if (locked_text_bubble == null) {
            TextBubbleLayout text_bubble = visible_text_layout.bubble as TextBubbleLayout;
            _hovering_text_view_origin = text_bubble.get_text_origin();

            _hovering_text_view.buffer.text = "";
            Gtk.TextIter start_iter;
            _hovering_text_view.buffer.get_start_iter(out start_iter);
            _hovering_text_view.buffer.insert_markup(ref start_iter, text_bubble.message.markup, -1);
            overlay.queue_allocate();
        }
    }

    private void reload_messages() {
        transcript_drawing_area.show_sender = _model.is_group_chat;
        transcript_drawing_area.set_messages(_model.messages);
    }
}
