using Gtk;
using Gee;
using Gdk;

private class TranscriptDrawingArea : Widget 
{
    public bool show_sender = true;
    public Adjustment? viewport {
        get {
            return _viewport;
        }

        set {
            _viewport = value;
            queue_draw();
        }
    }

    public signal void on_text_bubble_hover(VisibleLayout? text_bubble);
    public signal void on_text_bubble_click(VisibleLayout? text_bubble);
    public signal void on_image_bubble_activate(string attachment_guid);

    private ArrayList<Message> _messages = new ArrayList<Message>();
    private ArrayList<ChatItemLayout> _chat_items = new ArrayList<ChatItemLayout>();

    private Adjustment? _viewport = null;
    private const float bubble_margin = 18.0f;

    private GestureClick _click_gesture = new GestureClick();
    private Gdk.Rectangle? _click_bounding_box = null;

    private EventControllerMotion _motion_controller = new EventControllerMotion();
    private ArrayList<VisibleLayout?> _visible_text_layouts = new ArrayList<VisibleLayout?>();

    private const bool debug_viewport = false;
    private uint? _tick_callback_id = null;
    private HashMap<string, ChatItemAnimation> _animations = new HashMap<string, ChatItemAnimation>();

    public TranscriptDrawingArea() {
        add_css_class("transcript-drawing-area");
        
        weak TranscriptDrawingArea self = this;

        _click_gesture.button = 0;
        _click_gesture.pressed.connect((n_press, x, y) => {
            self.on_click(self._click_gesture.get_current_button(), n_press);
        });
        add_controller(_click_gesture);

        _motion_controller.motion.connect((x, y) => {
            if (self == null) {
                return;
            }

            // Only print motion events if window is active/focused
            var window = self.get_native() as Gtk.Window;
            if (window == null || !window.is_active) {
                return;
            }

            self.on_mouse_motion(x, y);
        });
        _motion_controller.set_propagation_phase(PropagationPhase.CAPTURE);
        add_controller(_motion_controller);

        SimpleAction copy_action = new SimpleAction("copy", null);
        copy_action.activate.connect(() => {
            if (_click_bounding_box != null) {
                copy_message_at(_click_bounding_box);
            } else {
                GLib.warning("Failed to get bounding box for right click");
            }
        });

        SimpleActionGroup action_group = new SimpleActionGroup();
        action_group.add_action(copy_action);

        insert_action_group("transcript", action_group);
    }

    public void set_messages(ArrayList<Message> messages) {
        _messages = messages;
        recompute_message_layouts();
    }

    public override SizeRequestMode get_request_mode() {
        return SizeRequestMode.HEIGHT_FOR_WIDTH;
    }
    
    public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) 
    {
        if (orientation == Orientation.HORIZONTAL) {
            // Horizontal, so we take up the full width provided
            minimum = 0;
            natural = for_size;
        } else {
            // compute total message layout height 
            float total_height = 0.0f;
            _chat_items.foreach((chat_item) => {
                total_height += chat_item.get_height() + chat_item.vertical_padding;
                return true;
            });

            minimum = (int)total_height;
            natural = (int)total_height;
        }
        
        minimum_baseline = -1;
        natural_baseline = -1;
    }

    public override void size_allocate(int width, int height, int baseline) {
        base.size_allocate(width, height, baseline);
        recompute_message_layouts();
    }

    public override void snapshot(Gtk.Snapshot snapshot) {
        const int viewport_y_padding = 50;
        var viewport_rect = Graphene.Rect() {
            origin = Graphene.Point() { x = 0, y = ((int)viewport.value - viewport_y_padding) },
            size = Graphene.Size() { width = get_width(), height = (int)viewport.page_size + (viewport_y_padding * 2) }
        };

        if (debug_viewport) {
            // Draw viewport outline for debugging
            var color = Gdk.RGBA() { red = 1.0f, green = 0, blue = 0, alpha = 1.0f };
            snapshot.append_border(
                Gsk.RoundedRect().init_from_rect(viewport_rect, 0),
                { 1, 1, 1, 1 },
                { color, color, color, color }
            );
        }

        // Draw each item in reverse order, since the messages are in reverse order
        float y_offset = 0;
        int container_width = get_width();
        _visible_text_layouts.clear();
        for (int i = _chat_items.size - 1; i >= 0; i--) {
            var chat_item = _chat_items[i];
            var item_width = chat_item.get_width();
            var item_height = chat_item.get_height();

            var origin = Graphene.Point() {
                x = (chat_item.from_me ? (container_width - item_width - bubble_margin) : bubble_margin), 
                y = y_offset 
            };

            var size = Graphene.Size() {
                width = item_width,
                height = item_height
            };

            var rect = Graphene.Rect() {
                origin = origin,
                size = size
            };

            // Skip drawing if this item is not in the viewport
            float height_offset = 0.0f;
            if (viewport_rect.intersection(rect, null)) {
                if (chat_item is BubbleLayout) {
                    _visible_text_layouts.add(VisibleLayout(chat_item as BubbleLayout, rect));
                }

                snapshot.save();

                var pushed_opacity = false;
                if (_animations.has_key(chat_item.id)) {
                    var animation = _animations[chat_item.id];

                    var item_height_offset = (float) (-item_height * (1.0 - animation.progress));
                    snapshot.translate(Graphene.Point() { x = 0, y = item_height_offset });
                    height_offset = item_height_offset;

                    snapshot.push_opacity(animation.progress);
                    pushed_opacity = true;
                }

                // Translate to the correct position
                snapshot.translate(origin);

                // Flip the y-axis, since our parent is upside down (so newest messages are at the bottom)
                snapshot.scale(1.0f, -1.0f);

                // Undo the y-axis flip, origin is top left 
                snapshot.translate(Graphene.Point() { x = 0, y = -item_height });

                chat_item.draw(snapshot);

                if (pushed_opacity) {
                    snapshot.pop();
                }

                snapshot.restore();
            }

            y_offset += item_height + chat_item.vertical_padding + height_offset;
        }

        animation_tick();
    }

    private VisibleLayout? get_visible_layout_at(double x, double y) {
        var point = Graphene.Point() { x = (float)x, y = (float)y };
        foreach (var layout in _visible_text_layouts) {
            if (layout.rect.contains_point(point)) {
                return layout;
            }
        }

        return null;
    }

    private VisibleLayout? get_text_bubble_at(double x, double y) {
        var layout = get_visible_layout_at(x, y);
        if (layout != null && layout.bubble is TextBubbleLayout) {
            return layout;
        }

        return null;
    }

    private void on_mouse_motion(double x, double y) {
        VisibleLayout? hovered_text_bubble = get_text_bubble_at(x, y);
        on_text_bubble_hover(hovered_text_bubble);
    }

    private void on_click(uint button, int n_press) {
        if (button == Gdk.BUTTON_SECONDARY) {
            on_right_click();
        } else if (button == Gdk.BUTTON_PRIMARY) {
            on_left_click();

            if (n_press == 2) {
                on_double_click();
            }
        }
    }

    private void on_left_click() {
        Gdk.Rectangle? bounding_box = null;
        if (_click_gesture.get_bounding_box(out bounding_box)) {
            var text_bubble = get_text_bubble_at(bounding_box.x, bounding_box.y);
            on_text_bubble_click(text_bubble);
        }
    }

    private void on_double_click() {
        Gdk.Rectangle? bounding_box = null;
        if (_click_gesture.get_bounding_box(out bounding_box)) {
            var double_clicked_bubble = get_visible_layout_at(bounding_box.x, bounding_box.y);
            if (double_clicked_bubble != null && double_clicked_bubble.bubble is ImageBubbleLayout) {
                var image_bubble = double_clicked_bubble.bubble as ImageBubbleLayout;
                on_image_bubble_activate(image_bubble.attachment_guid);
            }
        }
    }

    private void on_right_click() {
        var menu_model = new Menu();
        menu_model.append("Copy", "transcript.copy");

        Gdk.Rectangle? bounding_box = null;
        if (_click_gesture.get_bounding_box(out bounding_box)) {
            _click_bounding_box = bounding_box;

            var menu = new PopoverMenu.from_model(menu_model);
            menu.set_position(PositionType.TOP);
            menu.pointing_to = bounding_box;
            menu.set_parent(this);
            menu.popup();
        }
    }

    private void copy_message_at(Gdk.Rectangle bounding_box) {
        double y_offset = 0.0;
        for (int i = _chat_items.size - 1; i >= 0; i--) {
            var chat_item = _chat_items[i];
            y_offset += chat_item.get_height() + chat_item.vertical_padding;
            
            if (y_offset > bounding_box.y) {
                chat_item.copy(get_clipboard());
                break;
            }
        }
    }

    private bool animation_tick() {
        HashSet<string> animations_to_remove = new HashSet<string>();
        _animations.foreach(entry => {
            var animation = entry.value;
            animation.tick_animation();

            if ((animation.progress - 1.0).abs() < 0.000001) {
                animations_to_remove.add(entry.key);
            }

            return true;
        });

        foreach (var key in animations_to_remove) {
            _animations.unset(key);
        }

        if (_animations.size > 0) {
            queue_draw();
        }

        return _animations.size > 0;
    }

    private bool on_frame_clock_tick(Gtk.Widget widget, Gdk.FrameClock frame_clock) {
        return animation_tick();
    }

    private void start_animation(string id) {
        if (_animations.has_key(id)) {
            return;
        }

        var animation = new ChatItemAnimation(get_frame_clock());
        animation.start_animation(0.18f);
        _animations.set(id, animation);
        
        _tick_callback_id = add_tick_callback(on_frame_clock_tick);
    }

    private void recompute_message_layouts() {
        var container_width = get_width();
        float max_width = container_width * 0.80f;
        float image_max_width = max_width * 0.70f;

        DateTime? last_date = null;
        string? last_sender = null;
        ArrayList<ChatItemLayout> items = new ArrayList<ChatItemLayout>();
        _messages.foreach((message) => {
            // Date Annotation
            DateTime date = message.date;
            if (last_date != null && date.difference(last_date) > (TimeSpan.HOUR * 1)) {
                var date_item = new DateItemLayout(date.to_local().format("%b %d, %Y at %H:%M"), this, max_width);
                items.add(date_item);
                last_date = date;
                last_sender = null;
            }

            // Sender Annotation
            if (show_sender && !message.from_me && last_sender != message.sender) {
                var sender_bubble = new SenderAnnotationLayout(message.sender, max_width, this);
                items.add(sender_bubble);
            }

            // Text Bubble
            var animate = message.should_animate;
            if (message.text.length > 0 && !message.is_attachment_marker) {
                var text_bubble = new TextBubbleLayout(message, this, max_width);
                text_bubble.id = @"text-$(message.guid)";
                if (animate) {
                    start_animation(text_bubble.id);
                }

                text_bubble.vertical_padding = (last_sender == message.sender) ? 4.0f : 10.0f;
                items.add(text_bubble);
            }

            // Check for attachments. For each one, add an image layout bubble
            foreach (var attachment in message.attachments) {
                Graphene.Size? image_size = null;
                if (attachment.metadata != null && attachment.metadata.attribution_info != null) {
                    image_size = Graphene.Size() {
                        width = attachment.metadata.attribution_info.width,
                        height = attachment.metadata.attribution_info.height
                    };
                }

                var image_layout = new ImageBubbleLayout(attachment.guid, message.from_me, this, image_max_width, image_size);
                image_layout.id = @"image-$(attachment.guid)";

                if (animate) {
                    start_animation(image_layout.id);
                }

                image_layout.is_downloaded = attachment.preview_downloaded;
                items.add(image_layout);
            }

            // New-message animation is a one-shot effect. Clear the flag after
            // scheduling bubble animations so later relayouts do not replay it.
            message.should_animate = false;

            last_sender = message.sender;
            last_date = date;

            return true;
        });

        _chat_items.clear();
        _chat_items.add_all(items);
        
        queue_draw();
        queue_resize();
    }
}

private class ChatItemAnimation 
{
    public double progress = 0.0;
    private int64 start_time = 0;
    private double duration = 0.0;
    private Gdk.FrameClock frame_clock = null;

    delegate double EaseFunction(double t);
    private EaseFunction ease_function = ease_out_quart;

    public ChatItemAnimation(Gdk.FrameClock frame_clock) {
        this.frame_clock = frame_clock;
        progress = 0.0f;
    }

    public void start_animation(double duration) {
        this.duration = duration;
        this.progress = 0.0;
        this.start_time = frame_clock.get_frame_time();
    }

    public void tick_animation() {
        double t = (frame_clock.get_frame_time() - start_time) / (duration * 1000000.0);
        t = double.min(1.0, double.max(0.0, t));
        progress = ease_function(t);
    }

    private static double ease_out_quart(double t) {
        return 1.0 - Math.pow(1.0 - t, 4);
    }
}

public struct VisibleLayout {
    public weak BubbleLayout bubble;
    public Graphene.Rect rect;

    public VisibleLayout(BubbleLayout bubble, Graphene.Rect rect) {
        this.bubble = bubble;
        this.rect = rect;
    }
}
