/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: 2020, 2025 elementary, Inc. (https://elementary.io)
 */

public class Gala.DwellClickTimer : Clutter.Actor, Clutter.Animatable {
    private const double BACKGROUND_OPACITY = 0.7;
    private const int BORDER_WIDTH_PX = 1;

    private const double START_ANGLE = 3 * Math.PI / 2;

    /**
     * Delay, in milliseconds, before showing the animation.
     * libinput uses a timeout of 180ms when tapping is enabled. Use that value plus a safety
     * margin so the animation is never displayed when tapping.
     */
    private const double DELAY_TIMEOUT = 185;

    private float scaling_factor = 1.0f;
    private int cursor_size = 24;

    private Cogl.Pipeline pipeline;
    private Clutter.PropertyTransition transition;
    private Cairo.Pattern stroke_color;
    private Cairo.Pattern fill_color;
    private GLib.Settings interface_settings;
    private Cairo.ImageSurface surface;

    public Meta.Display display { get; construct; }

    public double angle { get; set; }

    public DwellClickTimer (Meta.Display display) {
        Object (display: display);
    }

    construct {
        visible = false;
        reactive = false;

#if HAS_MUTTER47
        unowned var backend = context.get_backend ();
#else
        unowned var backend = Clutter.get_default_backend ();
#endif

        pipeline = new Cogl.Pipeline (backend.get_cogl_context ());

        transition = new Clutter.PropertyTransition ("angle");
        transition.set_progress_mode (Clutter.AnimationMode.EASE_OUT_QUAD);
        transition.set_animatable (this);
        transition.set_from_value (START_ANGLE);
        transition.set_to_value (START_ANGLE + (2 * Math.PI));

        transition.new_frame.connect (() => {
            queue_redraw ();
        });

        interface_settings = new GLib.Settings ("org.gnome.desktop.interface");

        var seat = backend.get_default_seat ();
        seat.set_pointer_a11y_dwell_click_type (Clutter.PointerA11yDwellClickType.PRIMARY);

        seat.ptr_a11y_timeout_started.connect ((device, type, timeout) => {
            var scale = Utils.get_ui_scaling_factor (display, display.get_current_monitor ());
            update_cursor_size (scale);

#if HAS_MUTTER48
            unowned var tracker = display.get_compositor ().get_backend ().get_cursor_tracker ();
#else
            unowned var tracker = display.get_cursor_tracker ();
#endif
            Graphene.Point coords = {};
            tracker.get_pointer (out coords, null);

            x = coords.x - (width / 2);
            y = coords.y - (width / 2);

            transition.set_duration (timeout);
            visible = true;
            transition.start ();
        });

        seat.ptr_a11y_timeout_stopped.connect ((device, type, clicked) => {
            transition.stop ();
            visible = false;
        });
    }

    private void update_cursor_size (float scale) {
        scaling_factor = scale;

        cursor_size = (int) (interface_settings.get_int ("cursor-size") * scaling_factor * 1.25);

        if (surface == null || surface.get_width () != cursor_size || surface.get_height () != cursor_size) {
            surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, cursor_size, cursor_size);
        }

        set_size (cursor_size, cursor_size);
    }

    public override void paint (Clutter.PaintContext context) {
        if (angle == 0) {
            return;
        }

        var rgba = Drawing.StyleManager.get_instance ().theme_accent_color;
        var red = rgba.red / 255.0, green = rgba.green / 255.0, blue = rgba.blue / 255.0;
        stroke_color = new Cairo.Pattern.rgb (red, green, blue);
        /* Don't use alpha from the stylesheet to ensure contrast */
        fill_color = new Cairo.Pattern.rgba (red, green, blue, BACKGROUND_OPACITY);

        var radius = int.min (cursor_size / 2, cursor_size / 2);
        var end_angle = START_ANGLE + angle;
        var border_width = Utils.scale_to_int (BORDER_WIDTH_PX, scaling_factor);

        var cr = new Cairo.Context (surface);

        // Clear the surface
        cr.save ();
        cr.set_source_rgba (0, 0, 0, 0);
        cr.set_operator (Cairo.Operator.SOURCE);
        cr.paint ();
        cr.restore ();

        cr.set_line_cap (Cairo.LineCap.ROUND);
        cr.set_line_join (Cairo.LineJoin.ROUND);
        cr.translate (cursor_size / 2, cursor_size / 2);

        cr.move_to (0, 0);
        cr.arc (0, 0, radius - border_width, START_ANGLE, end_angle);
        cr.line_to (0, 0);
        cr.close_path ();

        cr.set_line_width (0);
        cr.set_source (fill_color);
        cr.fill_preserve ();

        cr.set_line_width (border_width);
        cr.set_source (stroke_color);
        cr.stroke ();

        var cogl_context = context.get_framebuffer ().get_context ();

        try {
            var texture = new Cogl.Texture2D.from_data (cogl_context, cursor_size, cursor_size, Cogl.PixelFormat.BGRA_8888_PRE,
                surface.get_stride (), surface.get_data ());

            pipeline.set_layer_texture (0, texture);

            context.get_framebuffer ().draw_rectangle (pipeline, 0, 0, cursor_size, cursor_size);
        } catch (Error e) {}

        base.paint (context);
    }

    public bool interpolate_value (string property_name, Clutter.Interval interval, double progress, out Value @value) {
        if (property_name == "angle") {
            @value = 0;

            var elapsed_time = transition.get_elapsed_time ();
            if (elapsed_time > DELAY_TIMEOUT) {
                double delayed_progress = (elapsed_time - DELAY_TIMEOUT) / (transition.duration - DELAY_TIMEOUT);
                @value = (delayed_progress * 2 * Math.PI);
            }

            return true;
        }

        return base.interpolate_value (property_name, interval, progress, out @value);
    }
}
