/*
 * Copyright (c) 2013, 2025 focus-timer contributors
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

using GLib;


namespace Ft
{
    private static int _is_flatpak = -1;
    private static string? _desktop_name = null;


    public delegate bool FilterFunc<T> (T item);


    public inline void ensure_timestamp (ref int64 timestamp)
    {
        if (Ft.Timestamp.is_undefined (timestamp)) {
            timestamp = Ft.Timestamp.from_now ();
        }
    }


    public inline string ensure_string (string? str)
    {
        return str != null ? str : "";
    }


    /**
     * Round seconds to 1s, 5s, 10s, 1m.
     *
     * Its intended for displaying rough estimation of duration.
     */
    // XXX: rename, name conflicts with Interval.round_seconds
    public double round_seconds (double seconds)
    {
        if (seconds < 10.0) {
            return Math.round (seconds);
        }

        if (seconds < 30.0) {
            return 5.0 * Math.round (seconds / 5.0);
        }

        if (seconds < 60.0) {
            return 10.0 * Math.round (seconds / 10.0);
        }

        return 60.0 * Math.round (seconds / 60.0);
    }


    /**
     * Convert seconds to text.
     *
     * If hours are present, seconds are omitted.
     */
    // XXX: rename to `format_seconds` ?
    public string format_time (uint seconds)  // TODO: rename to format_interval
    {
        var hours = seconds / 3600;
        var minutes = (seconds % 3600) / 60;
        var str = "";

        seconds = seconds % 60;

        if (hours > 0) {
            str = ngettext ("%u hour", "%u hours", hours).printf (hours);
        }

        if (minutes > 0)
        {
            if (str != "") {
                str += " ";
            }

            str += ngettext ("%u minute", "%u minutes", minutes).printf (minutes);
        }

        if ((seconds > 0 && hours == 0) || (seconds == 0 && minutes == 0 && hours == 0))
        {
            if (str != "") {
                str += " ";
            }

            str += ngettext ("%u second", "%u seconds", seconds).printf (seconds);
        }

        return str;
    }


    public inline double lerp (double value_from,
                               double value_to,
                               double t)
    {
        return value_from + (value_to - value_from) * t;
    }


    public inline float lerpf (float value_from,
                               float value_to,
                               float t)
    {
        return value_from + (value_to - value_from) * t;
    }


    public bool is_flatpak ()
    {
        if (_is_flatpak < 0) {
            var value = GLib.Environment.get_variable ("container") == "flatpak" &&
                        GLib.Environment.get_variable ("G_TEST_ROOT_PROCESS") == null;
            _is_flatpak = value ? 1 : 0;
        }

        return _is_flatpak > 0;
    }


    internal bool is_test ()
    {
        return GLib.Environment.get_variable ("G_TEST_ROOT_PROCESS") != null ||
               GLib.Environment.get_variable ("G_TEST_BUILDDIR") != null ||
               GLib.Environment.get_variable ("MESON_TEST_ITERATION") != null;
    }


    public bool is_devel ()
    {
        return Config.APPLICATION_ID.has_suffix (".Devel");
    }


    public string get_desktop_name ()
    {
        if (_desktop_name == null)
        {
            var desktop_name = GLib.Environment.get_variable ("XDG_SESSION_DESKTOP") ?? "";

            if (desktop_name == "") {
                desktop_name = GLib.Environment.get_variable ("XDG_CURRENT_DESKTOP") ?? "";
            }

            desktop_name = desktop_name.ascii_down ();

            switch (desktop_name)
            {
                case "ubuntu":
                    desktop_name = "gnome";
                    break;

                case "xubuntu":
                    desktop_name = "xfce";
                    break;

                case "lubuntu":
                    desktop_name = "lxqt";
                    break;

                case "budgie-desktop":
                    desktop_name = "budgie";
                    break;

                default:
                    break;
            }

            _desktop_name = desktop_name;
        }

        return _desktop_name;
    }


    public string to_camel_case (string name)
    {
        var     result = new GLib.StringBuilder ();
        var     was_hyphen = false;
        unichar chr;
        int     chr_span_end = 0;

        while (name.get_next_char (ref chr_span_end, out chr))
        {
            if (chr == '-') {
                was_hyphen = true;
                continue;
            }

            if (was_hyphen) {
                was_hyphen = false;
                result.append_unichar (chr.toupper ());
            }
            else {
                result.append_unichar (chr);
            }
        }

        return result.str;
    }


    public string from_camel_case (string name)
    {
        var     result = new GLib.StringBuilder ();
        var     was_lowercase = false;
        unichar chr;
        int     chr_span_end = 0;

        while (name.get_next_char (ref chr_span_end, out chr))
        {
            if (chr.isupper () && was_lowercase) {
                was_lowercase = false;
                result.append_c ('-');
                result.append_unichar (chr.tolower ());
            }
            else {
                was_lowercase = chr.islower ();
                result.append_unichar (was_lowercase ? chr : chr.tolower ());
            }
        }

        return result.str;
    }


    public delegate void ExtensionSetForeachFunc (GLib.Object object);

    /**
     * Bindings for `Peas.ExtensionSet.@foreach` seem to be broken.
     *
     * Here's an simpler, Vala-friendly implementation.
     */
    public void foreach_extension (Peas.ExtensionSet       extension_set,
                                   ExtensionSetForeachFunc func)
    {
        var n_items = extension_set.get_n_items ();

        for (var i = 0U; i < n_items; i++)
        {
            var object = extension_set.get_item (i);

            func (object);
        }
    }


    /**
     * A convenience wrapper around GLib.Queue that provides an async wait()
     * which resolves in the main loop once the queue becomes empty.
     */
    public class AsyncQueue<T>
    {
        private GLib.Queue<T>                        queue;
        private GLib.Mutex                           mutex;
        private GLib.Cond                            cond;
        private bool                                 in_dispose = false;
        private GLib.GenericArray<GLib.Thread<bool>> waiting_threads;

        public AsyncQueue ()
        {
            this.queue = new GLib.Queue<T> ();
            this.mutex = GLib.Mutex ();
            this.cond = GLib.Cond ();
            this.waiting_threads = new GLib.GenericArray<GLib.Thread<bool>> ();
        }

        ~AsyncQueue ()
        {
            this.mutex.lock ();
            this.in_dispose = true;
            this.cond.broadcast ();
            this.mutex.unlock ();

            foreach (var thread in this.waiting_threads) {
                thread.join ();
            }
        }

        /**
         * Push an item to the tail of the queue.
         */
        public void push (owned T item)
        {
            this.mutex.lock ();
            this.queue.push_tail ((owned) item);
            this.mutex.unlock ();
        }

        /**
         * Try to pop an item without blocking. Returns null if empty.
         */
        public T? pop ()
        {
            this.mutex.lock ();
            T? item = null;

            if (this.queue.get_length () > 0)
            {
                item = this.queue.pop_head ();

                if (this.queue.get_length () == 0) {
                    this.cond.broadcast ();
                }
            }

            this.mutex.unlock ();

            return item;
        }

        /**
         * Current length of the queue.
         */
        public uint length ()
        {
            this.mutex.lock ();
            var length = this.queue.get_length ();
            this.mutex.unlock ();

            return length;
        }

        /**
         * Asynchronously waits until the queue becomes empty.
         *
         * The completion is dispatched back to the main loop.
         */
        public async void wait ()
        {
            if (this.in_dispose) {
                return;
            }

            // Fast-path: already empty
            this.mutex.lock ();
            var already_empty = this.queue.get_length () == 0;
            this.mutex.unlock ();

            if (already_empty) {
                return;
            }

            SourceFunc callback = this.wait.callback;

            var thread = new GLib.Thread<bool> (
                "async-queue-wait",
                () => {
                    this.mutex.lock ();
                    while (this.queue.get_length () > 0 && !this.in_dispose) {
                        this.cond.wait (this.mutex);
                    }
                    this.mutex.unlock ();

                    // Schedule the callback in the main loop
                    var idle_source = new GLib.IdleSource ();
                    idle_source.set_name ("Ft.AsyncQueue.wait");
                    idle_source.set_callback ((owned) callback);
                    idle_source.attach (GLib.MainContext.@default ());

                    return true;
                });

            this.mutex.lock ();
            this.waiting_threads.add (thread);
            this.mutex.unlock ();

            // Wait for the callback
            yield;

            this.mutex.lock ();
            this.waiting_threads.remove_fast (thread);
            this.mutex.unlock ();
        }
    }
}
