/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*-
 *
 * Copyright (C) 2011,2012 Canonical Ltd
 * Copyright (C) 2015-2017 Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
 * Copyright (C) 2023 Robert Tari
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Robert Ancell <robert.ancell@canonical.com>
 *          Michael Terry <michael.terry@canonical.com>
 *          Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
 *          Robert Tari <robert@tari.in>
 */

private class IndicatorMenuItem : Gtk.MenuItem
{
    public unowned Indicator.ObjectEntry entry;
    private Gtk.Box hbox;

    public IndicatorMenuItem (Indicator.ObjectEntry entry)
    {
        this.entry = entry;
        this.hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 3);
        this.add (this.hbox);
        this.hbox.show ();

        if (entry.label != null)
        {
            entry.label.show.connect (this.visibility_changed_cb);
            entry.label.hide.connect (this.visibility_changed_cb);
            hbox.pack_start (entry.label, false, false, 0);
        }
        if (entry.image != null)
        {
            entry.image.show.connect (visibility_changed_cb);
            entry.image.hide.connect (visibility_changed_cb);
            hbox.pack_start (entry.image, false, false, 0);
        }
        if (entry.accessible_desc != null)
            get_accessible ().set_name (entry.accessible_desc);
        if (entry.menu != null)
            set_submenu (entry.menu);

        if (has_visible_child ())
            show ();
    }

    public bool has_visible_child ()
    {
        return (entry.image != null && entry.image.get_visible ()) ||
               (entry.label != null && entry.label.get_visible ());
    }

    public void visibility_changed_cb (Gtk.Widget widget)
    {
        visible = has_visible_child ();
    }
}

public class MenuBar : Gtk.MenuBar
{
    public Background? background { get; construct; default = null; }
    public Gtk.Window? keyboard_window { get; private set; default = null; }
    public Gtk.AccelGroup? accel_group { get; construct; }

    private const int HEIGHT = 32;

    public MenuBar (Background bg, Gtk.AccelGroup ag)
    {
        Object (background: bg, accel_group: ag);
    }

    public override bool draw (Cairo.Context c)
    {
        if (background != null)
        {
            /* Disable background drawing to see how it changes the visuals. */
            /*
            int x, y;
            background.translate_coordinates (this, 0, 0, out x, out y);
            c.save ();
            c.translate (x, y);
            background.draw_full (c, Background.DrawFlags.NONE);
            c.restore ();
            */
        }

        /* Get the style and dimensions. */
        var style_ctx = this.get_style_context ();

        var w = this.get_allocated_width ();
        var h = this.get_allocated_height ();

        /* Add a group. */
        c.push_group ();

        /* Draw the background normally. */
        style_ctx.render_background (c, 0, 0, w, h);

        /* Draw the frame normally. */
        style_ctx.render_frame (c, 0, 0, w, h);

        /* Go back to the original widget. */
        c.pop_group_to_source ();

        var agsettings = new AGSettings ();
        if (agsettings.high_contrast) {
            /*
             * In case the high contrast mode is enabled, do not add any
             * transparency. While the GTK theme might define one (even though
             * it better should not, given that we are also switching to a
             * high contrast theme), we certainly do not want to make the look
             * fuzzy.
             */
             c.paint ();
        }
        else {
            /*
             * And finally repaint it with additional transparency.
             * Note that most GTK styles already define a transparency for OSD
             * menus. We want to have something more transparent, but also
             * make sure that it is not too transparent, so do not choose a
             * value that is too low here - certainly not your desired final
             * alpha value.
             */
            c.paint_with_alpha (AGSettings.get_double (AGSettings.KEY_MENUBAR_ALPHA));
        }

        foreach (var child in get_children ())
        {
            propagate_draw (child, c);
        }

        return false;
    }

    public static void add_style_class (Gtk.Widget widget)
    {
        /*
         * Add style context class osd, which makes the widget respect the GTK
         * style definitions for this type of elements.
         */
        var ctx = widget.get_style_context ();
        ctx.add_class ("osd");
    }

    private List<Indicator.Object> indicator_objects;

    construct
    {
        add_style_class (this);

        /* Add shadow. */
        var shadow_style = new Gtk.CssProvider ();

        try
        {
            shadow_style.load_from_data ("* { box-shadow: 0px 0px 5px 5px #000000; }", -1);
        }
        catch (Error pError)
        {
            error ("Panic: Failed adding shadow: %s", pError.message);
        }

        this.get_style_context ().add_provider (shadow_style,
                                                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);

        pack_direction = Gtk.PackDirection.RTL;

        if (AGSettings.get_boolean (AGSettings.KEY_SHOW_HOSTNAME))
        {
            var hostname_item = new Gtk.MenuItem.with_label (Posix.utsname ().nodename);
            append (hostname_item);
            hostname_item.show ();

            /*
             * Even though this (menu) item is insensitive, we want its label
             * text to have the sensitive color as to not look out of place
             * and difficult to read.
             *
             * There's a really weird bug that leads to always fetch the
             * sensitive color after the widget (menuitem in this case) has
             * been set to insensitive once - at least in this constructor.
             *
             * I haven't found a way to fix that, or, for that matter, what is
             * actually causing the issue. Even waiting on the main event loop
             * until all events are processed didn't help.
             *
             * We'll work around this issue by fetching the color before
             * setting the widget to insensitive and call it proper.
             */
            var insensitive_override_style = new Gtk.CssProvider ();

            /*
             * First, fetch the associated GtkStyleContext and save the state,
             * we'll override the state later on.
             */
            var hostname_item_ctx = hostname_item.get_style_context ();
            hostname_item_ctx.save ();

            try {
                /* Get the actual color. */
                var sensitive_color = hostname_item_ctx.get_color (Gtk.StateFlags.NORMAL);
                debug ("Directly fetched sensitive color: %s", sensitive_color.to_string ());

                insensitive_override_style.load_from_data ("*:disabled { color: %s; }".printf(sensitive_color.to_string ()), -1);
            }
            catch (Error e)
            {
                debug ("Internal error loading hostname menu item text color: %s", e.message);
            }
            finally {
                /*
                 * Restore the context, which we might have changed through the
                 * previous get_color () call.
                 */
                hostname_item_ctx.restore ();
            }

            try {
                /* And finally override the insensitive color. */
                hostname_item_ctx.add_provider (insensitive_override_style,
                                                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);

                /*
                 * Just overriding the color for the Gtk.MenuItem widget
                 * doesn't help, we'll also apply it to the children.
                 *
                 * In theory, we could just use the get_child () method to
                 * fetch the only child we should ever have on that widget,
                 * namely a GtkAccelLabel, but that isn't future-proof enough,
                 * especially if that is ever extended into having a submenu.
                 *
                 * Thus, iterate over all children and override the style for
                 * all of them.
                 */
                if (gtk_is_container (hostname_item)) {
                    var children = hostname_item.get_children ();
                    foreach (Gtk.Widget element in children) {
                        var child_ctx = element.get_style_context ();
                        debug ("Adding override style provider to child widget %s", element.name);
                        child_ctx.add_provider (insensitive_override_style,
                                                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
                    }
                }
            }
            catch (Error e)
            {
                debug ("Internal error overriding hostname menu item text color: %s", e.message);
            }

            hostname_item.set_sensitive (false);

            /* The below does not work, so for now we need to stick to "set_right_justified"
            hostname_item.set_hexpand (true);
            hostname_item.set_halign (Gtk.Align.END);*/
            hostname_item.set_right_justified (true);
        }

        /* Prevent dragging the window by the menubar */
        try
        {
            var style = new Gtk.CssProvider ();
            style.load_from_data ("* {-GtkWidget-window-dragging: false;}", -1);
            get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
        }
        catch (Error e)
        {
            debug ("Internal error loading menubar style: %s", e.message);
        }

        setup_indicators ();
    }

    public override void get_preferred_height (out int min, out int nat)
    {
        min = HEIGHT;
        nat = HEIGHT;
    }

    private void greeter_set_env (string key, string val)
    {
        GLib.Environment.set_variable (key, val, true);

        /* And also set it in the DBus activation environment so that any
         * indicator services pick it up. */
        try
        {
            var proxy = new GLib.DBusProxy.for_bus_sync (GLib.BusType.SESSION,
                                                         GLib.DBusProxyFlags.NONE, null,
                                                         "org.freedesktop.DBus",
                                                         "/org/freedesktop/DBus",
                                                         "org.freedesktop.DBus",
                                                         null);

            var builder = new GLib.VariantBuilder (GLib.VariantType.ARRAY);
            builder.add ("{ss}", key, val);

            proxy.call_sync ("UpdateActivationEnvironment", new GLib.Variant ("(a{ss})", builder), GLib.DBusCallFlags.NONE, -1, null);
        }
        catch (Error e)
        {
            warning ("Could not get set environment for indicators: %s", e.message);
            return;
        }
    }

    private Indicator.Object? load_indicator_file (string indicator_name)
    {
        string dir = Config.INDICATOR_FILE_DIR;
        string path;
        Indicator.Object io;

        /* To stay backwards compatible, use org.ayatana.indicator as the default prefix */
        if (indicator_name.index_of_char ('.') < 0)
            path = @"$dir/org.ayatana.indicator.$indicator_name";
        else
            path = @"$dir/$indicator_name";

        try
        {
            io = new Indicator.Ng.for_profile (path, "desktop_greeter");
        }
        catch (FileError error)
        {
            /* the calling code handles file-not-found; don't warn here */
            return null;
        }
        catch (Error error)
        {
            warning ("unable to load %s: %s", indicator_name, error.message);
            return null;
        }

        return io;
    }

    private Indicator.Object? load_indicator_library (string indicator_name)
    {
        // Find file, if it exists
        string[] names_to_try = {"lib" + indicator_name + ".so",
                                 indicator_name + ".so",
                                 indicator_name};
        foreach (var filename in names_to_try)
        {
            var full_path = Path.build_filename (Config.INDICATORDIR, filename);
            var io = new Indicator.Object.from_file (full_path);
            if (io != null)
                return io;
        }

        return null;
    }

    private void load_indicator (string indicator_name)
    {
        var greeter = new ArcticaGreeter ();
        if (!greeter.test_mode)
        {
            var io = load_indicator_file (indicator_name);

            if (io == null)
                io = load_indicator_library (indicator_name);

            if (io != null)
            {
                indicator_objects.append (io);
                io.entry_added.connect (indicator_added_cb);
                io.entry_removed.connect (indicator_removed_cb);
                foreach (var entry in io.get_entries ())
                    indicator_added_cb (io, entry);
            }
        }
    }

    private void setup_indicators ()
    {
        /* Set indicators to run with reduced functionality */
        greeter_set_env ("INDICATOR_GREETER_MODE", "1");

        /* Don't allow virtual file systems? */
        greeter_set_env ("GIO_USE_VFS", "local");
        greeter_set_env ("GVFS_DISABLE_FUSE", "1");

        /* Hint to have mate-settings-daemon run in greeter mode */
        greeter_set_env ("RUNNING_UNDER_GDM", "1");

        /* Let indicators know about our unique dbus name */
        try
        {
            var conn = Bus.get_sync (BusType.SESSION);
            greeter_set_env ("ARCTICA_GREETER_DBUS_NAME", conn.get_unique_name ());
        }
        catch (IOError e)
        {
            debug ("Could not set DBUS_NAME: %s", e.message);
        }

        debug ("LANG=%s LANGUAGE=%s", Environment.get_variable ("LANG"), Environment.get_variable ("LANGUAGE"));

        var indicator_list = AGSettings.get_strv(AGSettings.KEY_INDICATORS);

        foreach (var indicator in indicator_list)
            load_indicator(indicator);

       indicator_objects.sort((a, b) => {
           int pos_a = a.get_position ();
           int pos_b = b.get_position ();

           if (pos_a < 0)
               pos_a = 1000;
           if (pos_b < 0)
               pos_b = 1000;

           return pos_a - pos_b;
        });

        debug ("LANG=%s LANGUAGE=%s", Environment.get_variable ("LANG"), Environment.get_variable ("LANGUAGE"));
    }

    private uint get_indicator_index (Indicator.Object object)
    {
        uint index = 0;

        foreach (var io in indicator_objects)
        {
            if (io == object)
                return index;
            index++;
        }

        return index;
    }

    private Indicator.Object? get_indicator_object_from_entry (Indicator.ObjectEntry entry)
    {
        foreach (var io in indicator_objects)
        {
            foreach (var e in io.get_entries ())
            {
                if (e == entry)
                    return io;
            }
        }

        return null;
    }

    private void indicator_added_cb (Indicator.Object object, Indicator.ObjectEntry entry)
    {
        var index = get_indicator_index (object);
        var pos = 0;
        foreach (var child in get_children ())
        {
            if (!(child is IndicatorMenuItem))
                break;

            var menuitem = (IndicatorMenuItem) child;
            var child_object = get_indicator_object_from_entry (menuitem.entry);
            var child_index = get_indicator_index (child_object);
            if (child_index > index)
                break;
            pos++;
        }

        debug ("Adding indicator object %p at position %d", entry, pos);

        var menuitem = new IndicatorMenuItem (entry);
        insert (menuitem, pos);
    }

    private void indicator_removed_cb (Indicator.Object object, Indicator.ObjectEntry entry)
    {
        debug ("Removing indicator object %p", entry);

        foreach (var child in get_children ())
        {
            var menuitem = (IndicatorMenuItem) child;
            if (menuitem.entry == entry)
            {
                remove (child);
                return;
            }
        }

        warning ("Indicator object %p not in menubar", entry);
    }
}