/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*-
 *
 * Copyright (C) 2013 Canonical Ltd
 * Copyright (C) 2015,2016 Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
 *
 * 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>
 *          Marco Trevisan <marco.trevisan@canonical.com>
 *          Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
 */

public enum ShutdownDialogType
{
    LOGOUT,
    SHUTDOWN,
    RESTART
}

public class ShutdownDialog : Gtk.Fixed
{
    public signal void closed ();

    private Cairo.ImageSurface? bg_surface = null;
    private Cairo.ImageSurface? corner_surface = null;
    private Cairo.ImageSurface? left_surface = null;
    private Cairo.ImageSurface? top_surface = null;
    private Cairo.Pattern? corner_pattern = null;
    private Cairo.Pattern? left_pattern = null;
    private Cairo.Pattern? top_pattern = null;

    private const int BORDER_SIZE = 30;
    private const int BORDER_INTERNAL_SIZE = 10;
    private const int BORDER_EXTERNAL_SIZE = BORDER_SIZE - BORDER_INTERNAL_SIZE;
    private const int CLOSE_OFFSET = 3;
    private const int BUTTON_TEXT_SPACE = 9;
    private const int BLUR_RADIUS = 8;

    private Monitor monitor;
    private weak Background background;
    private Gdk.RGBA avg_color;

    private Gtk.Box vbox;
    private DialogButton close_button;
    private Gtk.Box button_box;
    private Gtk.EventBox monitor_events;
    private Gtk.EventBox vbox_events;

    private AnimateTimer animation;
    private bool closing = false;


    public ShutdownDialog (ShutdownDialogType type, Background bg)
    {
        background = bg;
        background.notify["alpha"].connect (rebuild_background);
        background.notify["average-color"].connect (update_background_color);
        update_background_color ();

        // This event box covers the monitor size, and closes the dialog on click.
        monitor_events = new Gtk.EventBox ();
        monitor_events.visible = true;
        monitor_events.set_visible_window (false);
        monitor_events.events |= Gdk.EventMask.BUTTON_PRESS_MASK;
        monitor_events.button_press_event.connect (() => {
            close ();
            return true;
        });
        add (monitor_events);

        vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 10);
        vbox.visible = true;

        vbox.margin = BORDER_INTERNAL_SIZE;
        vbox.margin_top += 9;
        vbox.margin_left += 20;
        vbox.margin_right += 20;
        vbox.margin_bottom += 2;

        // This event box consumes the click events inside the vbox
        vbox_events = new Gtk.EventBox();
        vbox_events.visible = true;
        vbox_events.set_visible_window (false);
        vbox_events.events |= Gdk.EventMask.BUTTON_PRESS_MASK;
        vbox_events.button_press_event.connect (() => { return true; });
        vbox_events.add (vbox);
        monitor_events.add (vbox_events);

        string text;

        if (type == ShutdownDialogType.SHUTDOWN)
        {
            text = _("Goodbye. Would you like to…");
        }
        else
        {
            var title_label = new Gtk.Label (null);
            title_label.visible = true;
            title_label.set_markup ("<span font=\"Cantarell 15\" fgcolor=\"%s\">%s</span>".printf (AGSettings.get_string (AGSettings.KEY_TOGGLEBOX_FONT_FGCOLOR), _("Shut Down")));
            title_label.set_alignment (0.0f, 0.5f);
            vbox.pack_start (title_label, false, false, 0);

            text = _("Are you sure you want to shut down the computer?");
        }

        var have_open_sessions = false;
        try
        {
            var b = Bus.get_sync (BusType.SYSTEM);
            var result = b.call_sync ("org.freedesktop.DisplayManager",
                                      "/org/freedesktop/DisplayManager",
                                      "org.freedesktop.DBus.Properties",
                                      "Get",
                                      new Variant ("(ss)", "org.freedesktop.DisplayManager", "Sessions"),
                                      new VariantType ("(v)"),
                                      DBusCallFlags.NONE,
                                      -1,
                                      null);
            Variant value;
            result.get ("(v)", out value);
            have_open_sessions = value.n_children () > 0;
        }
        catch (Error e)
        {
            warning ("Failed to check sessions from logind: %s", e.message);
        }
        if (have_open_sessions)
            text = "%s\n\n%s".printf (_("Other users are currently logged in to this computer, shutting down now will also close these other sessions."), text);

        var label = new Gtk.Label (null);
        label.set_line_wrap (true);
        label.set_markup ("<span font=\"Cantarell 12\" fgcolor=\"%s\">%s</span>".printf (AGSettings.get_string (AGSettings.KEY_TOGGLEBOX_FONT_FGCOLOR), text));
        label.set_alignment (0.0f, 0.5f);
        label.visible = true;
        vbox.pack_start (label, false, false, 0);

        button_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 20);
        button_box.visible = true;
        vbox.pack_start (button_box, false, false, 0);

        if (type == ShutdownDialogType.SHUTDOWN)
        {
            if (LightDM.get_can_suspend ())
            {
                var button = add_button (_("Suspend"), Path.build_filename (Config.PKGDATADIR, "suspend.png"), Path.build_filename (Config.PKGDATADIR, "suspend_highlight.png"));
                button.clicked.connect (() =>
                {
                    try
                    {
                        LightDM.suspend ();
                        close ();
                    }
                    catch (Error e)
                    {
                        warning ("Failed to suspend: %s", e.message);
                    }
                });
            }

            if (LightDM.get_can_hibernate ())
            {
                var button = add_button (_("Hibernate"), Path.build_filename (Config.PKGDATADIR, "hibernate.png"), Path.build_filename (Config.PKGDATADIR, "hibernate_highlight.png"));
                button.clicked.connect (() =>
                {
                    try
                    {
                        LightDM.hibernate ();
                        close ();
                    }
                    catch (Error e)
                    {
                        warning ("Failed to hibernate: %s", e.message);
                    }
                });
            }
        }

        if (LightDM.get_can_restart ())
        {
            var button = add_button (_("Restart"), Path.build_filename (Config.PKGDATADIR, "restart.png"), Path.build_filename (Config.PKGDATADIR, "restart_highlight.png"));
            button.clicked.connect (() =>
            {
                try
                {
                    LightDM.restart ();
                    close ();
                }
                catch (Error e)
                {
                    warning ("Failed to restart: %s", e.message);
                }
            });
            if (type == ShutdownDialogType.RESTART)
                show.connect(() => { button.grab_focus (); });
        }

        if (LightDM.get_can_shutdown ())
        {
            var button = add_button (_("Shut Down"), Path.build_filename (Config.PKGDATADIR, "shutdown.png"), Path.build_filename (Config.PKGDATADIR, "shutdown_highlight.png"));
            button.clicked.connect (() =>
            {
                try
                {
                    LightDM.shutdown ();
                    close ();
                }
                catch (Error e)
                {
                    warning ("Failed to shutdown: %s", e.message);
                }
            });

            if (type == ShutdownDialogType.SHUTDOWN)
                show.connect(() => { button.grab_focus (); });
        }

        close_button = new DialogButton (Path.build_filename (Config.PKGDATADIR, "dialog_close.png"), Path.build_filename (Config.PKGDATADIR, "dialog_close_highlight.png"), Path.build_filename (Config.PKGDATADIR, "dialog_close_press.png"));
        close_button.can_focus = false;
        close_button.clicked.connect (() => { close (); });
        close_button.visible = true;
        add (close_button);

        animation = new AnimateTimer ((x) => { return x; }, AnimateTimer.INSTANT);
        animation.animate.connect (() => { queue_draw (); });
        show.connect (() => { animation.reset(); });
    }

    public void close ()
    {
        var start_value = 1.0f - animation.progress;
        animation = new AnimateTimer ((x) => { return start_value + x; }, AnimateTimer.INSTANT);
        animation.animate.connect ((p) =>
        {
            queue_draw ();

            if (p >= 1.0f)
            {
                animation.stop ();
                closed ();
            }
        });

        closing = true;
        animation.reset();
    }

    private void rebuild_background ()
    {
        bg_surface = null;
        queue_draw ();
    }

    private void update_background_color ()
    {
        // Apply the same color corrections we do in Unity
        // For reference, see unity's unity-shared/BGHash.cpp
        double hue, saturation, value;
        const double COLOR_ALPHA = 0.72f;

        Gdk.RGBA color = background.average_color;
        Gtk.RGB.to_hsv (color.red, color.green, color.blue,
                        out hue, out saturation, out value);

        if (saturation < 0.08)
        {
            // Got a grayscale image
            avg_color = {0.18f, 0.20f, 0.21f, COLOR_ALPHA };
        }
        else
        {
            const Gdk.RGBA[] cmp_colors =
            {
                {84/255.0f, 14/255.0f, 68/255.0f, 1.0f},
                {110/255.0f, 11/255.0f, 42/255.0f, 1.0f},
                {132/255.0f, 22/255.0f, 23/255.0f, 1.0f},
                {132/255.0f, 55/255.0f, 27/255.0f, 1.0f},
                {134/255.0f, 77/255.0f, 32/255.0f, 1.0f},
                {133/255.0f, 127/255.0f, 49/255.0f, 1.0f},
                {29/255.0f, 99/255.0f, 49/255.0f, 1.0f},
                {17/255.0f, 88/255.0f, 46/255.0f, 1.0f},
                {14/255.0f, 89/255.0f, 85/255.0f, 1.0f},
                {25/255.0f, 43/255.0f, 89/255.0f, 1.0f},
                {27/255.0f, 19/255.0f, 76/255.0f, 1.0f},
                {2/255.0f, 192/255.0f, 212/255.0f, 1.0f}
            };

            avg_color = {0, 0, 0, 1};
            double closest_diff = 200.0f;

            foreach (var c in cmp_colors)
            {
                double cmp_hue, cmp_sat, cmp_value;
                Gtk.RGB.to_hsv (c.red, c.green, c.blue,
                                out cmp_hue, out cmp_sat, out cmp_value);
                double color_diff = Math.fabs (hue - cmp_hue);

                if (color_diff < closest_diff)
                {
                    avg_color = c;
                    closest_diff = color_diff;
                }
            }

            double new_hue, new_saturation, new_value;
            Gtk.RGB.to_hsv (avg_color.red, avg_color.green, avg_color.blue,
                            out new_hue, out new_saturation, out new_value);

            saturation = double.min (saturation, new_saturation);
            saturation *= (2.0f - saturation);
            value = double.min (double.min (value, new_value), 0.26f);
            Gtk.HSV.to_rgb (hue, saturation, value,
                            out avg_color.red, out avg_color.green, out avg_color.blue);
            avg_color.alpha = COLOR_ALPHA;
        }

        rebuild_background ();
    }

    public void set_active_monitor (Monitor m)
    {
        if (m == this.monitor || m.equals (this.monitor))
            return;

        monitor = m;
        rebuild_background ();
        set_size_request (monitor.width, monitor.height);
    }

    public void focus_next ()
    {
        (get_toplevel () as Gtk.Window).move_focus (Gtk.DirectionType.TAB_FORWARD);
    }

    public void focus_prev ()
    {
        (get_toplevel () as Gtk.Window).move_focus (Gtk.DirectionType.TAB_BACKWARD);
    }

    public void cancel ()
    {
        var widget = (get_toplevel () as Gtk.Window).get_focus ();
        if (widget is DialogButton)
            (get_toplevel () as Gtk.Window).set_focus (null);
        else
            close ();
    }

    public override void size_allocate (Gtk.Allocation allocation)
    {
        base.size_allocate (allocation);
        monitor_events.size_allocate (allocation);

        var content_allocation = Gtk.Allocation ();
        int minimum_width, natural_width, minimum_height, natural_height;
        vbox_events.get_preferred_width (out minimum_width, out natural_width);
        vbox_events.get_preferred_height_for_width (minimum_width, out minimum_height, out natural_height);
        content_allocation.x = allocation.x + (allocation.width - minimum_width) / 2;
        content_allocation.y = allocation.y + (allocation.height - minimum_height) / 2;
        content_allocation.width = minimum_width;
        content_allocation.height = minimum_height;
        vbox_events.size_allocate (content_allocation);

        var a = Gtk.Allocation ();
        close_button.get_preferred_width (out minimum_width, out natural_width);
        close_button.get_preferred_height (out minimum_height, out natural_height);
        a.x = content_allocation.x - BORDER_EXTERNAL_SIZE + CLOSE_OFFSET;
        a.y = content_allocation.y - BORDER_EXTERNAL_SIZE + CLOSE_OFFSET;
        a.width = minimum_width;
        a.height = minimum_height;
        close_button.size_allocate (a);
    }

    public override bool draw (Cairo.Context c)
    {
        if (corner_surface == null)
        {
            corner_surface = new Cairo.ImageSurface.from_png (Path.build_filename (Config.PKGDATADIR, "switcher_corner.png"));
            left_surface = new Cairo.ImageSurface.from_png (Path.build_filename (Config.PKGDATADIR, "switcher_left.png"));
            top_surface = new Cairo.ImageSurface.from_png (Path.build_filename (Config.PKGDATADIR, "switcher_top.png"));
            corner_pattern = new Cairo.Pattern.for_surface (corner_surface);
            left_pattern = new Cairo.Pattern.for_surface (left_surface);
            left_pattern.set_extend (Cairo.Extend.REPEAT);
            top_pattern = new Cairo.Pattern.for_surface (top_surface);
            top_pattern.set_extend (Cairo.Extend.REPEAT);
        }

        int width = vbox_events.get_allocated_width ();
        int height = vbox_events.get_allocated_height ();
        int x = (get_allocated_width () - width) / 2;
        int y = (get_allocated_height () - height) / 2;

        if (animation.is_running)
            c.push_group ();

        /* Darken background */
        c.set_source_rgba (0, 0, 0, 0.25);
        c.paint ();

        if (bg_surface == null || animation.is_running)
        {
            /* Create a new blurred surface of the current surface */
            bg_surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, width, height);
            var bg_cr = new Cairo.Context (bg_surface);

            bg_cr.set_source_surface (c.get_target (), -x - monitor.x, -y - monitor.y);
            bg_cr.rectangle (0, 0, width, height);
            bg_cr.fill ();

            CairoUtils.ExponentialBlur.surface (bg_surface, BLUR_RADIUS);
        }

        /* Background */
        c.save ();
        c.translate (x, y);

        CairoUtils.rounded_rectangle (c, 0, 0, width, height, 4);
        c.set_source_surface (bg_surface, 0, 0);
        c.fill_preserve ();
        c.set_source_rgba (avg_color.red, avg_color.green, avg_color.blue, avg_color.alpha);
        c.fill ();

        c.restore();

        /* Draw borders */
        x -= BORDER_EXTERNAL_SIZE;
        y -= BORDER_EXTERNAL_SIZE;
        width += BORDER_EXTERNAL_SIZE * 2;
        height += BORDER_EXTERNAL_SIZE * 2;

        c.save ();
        c.translate (x, y);

        /* Top left */
        var m = Cairo.Matrix.identity ();
        corner_pattern.set_matrix (m);
        c.set_source (corner_pattern);
        c.rectangle (0, 0, BORDER_SIZE, BORDER_SIZE);
        c.fill ();

        /* Top right */
        m = Cairo.Matrix.identity ();
        m.translate (width, 0);
        m.scale (-1, 1);
        corner_pattern.set_matrix (m);
        c.set_source (corner_pattern);
        c.rectangle (width - BORDER_SIZE, 0, BORDER_SIZE, BORDER_SIZE);
        c.fill ();

        /* Bottom left */
        m = Cairo.Matrix.identity ();
        m.translate (0, height);
        m.scale (1, -1);
        corner_pattern.set_matrix (m);
        c.set_source (corner_pattern);
        c.rectangle (0, height - BORDER_SIZE, BORDER_SIZE, BORDER_SIZE);
        c.fill ();

        /* Bottom right */
        m = Cairo.Matrix.identity ();
        m.translate (width, height);
        m.scale (-1, -1);
        corner_pattern.set_matrix (m);
        c.set_source (corner_pattern);
        c.rectangle (width - BORDER_SIZE, height - BORDER_SIZE, BORDER_SIZE, BORDER_SIZE);
        c.fill ();

        /* Left */
        m = Cairo.Matrix.identity ();
        left_pattern.set_matrix (m);
        c.set_source (left_pattern);
        c.rectangle (0, BORDER_SIZE, BORDER_SIZE, height - BORDER_SIZE * 2);
        c.fill ();

        /* Right */
        m = Cairo.Matrix.identity ();
        m.translate (width, 0);
        m.scale (-1, 1);
        left_pattern.set_matrix (m);
        c.set_source (left_pattern);
        c.rectangle (width - BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, height - BORDER_SIZE * 2);
        c.fill ();

        /* Top */
        m = Cairo.Matrix.identity ();
        top_pattern.set_matrix (m);
        c.set_source (top_pattern);
        c.rectangle (BORDER_SIZE, 0, width - BORDER_SIZE * 2, BORDER_SIZE);
        c.fill ();

        /* Bottom */
        m = Cairo.Matrix.identity ();
        m.translate (0, height);
        m.scale (1, -1);
        top_pattern.set_matrix (m);
        c.set_source (top_pattern);
        c.rectangle (BORDER_SIZE, height - BORDER_SIZE, width - BORDER_SIZE * 2, BORDER_SIZE);
        c.fill ();

        c.restore ();

        var ret = base.draw (c);

        if (animation.is_running)
        {
            c.pop_group_to_source ();
            c.paint_with_alpha (closing ? 1.0f - animation.progress : animation.progress);
        }

        return ret;
    }

    private DialogButton add_button (string text, string inactive_filename, string active_filename)
    {
        var b = new Gtk.Box (Gtk.Orientation.VERTICAL, BUTTON_TEXT_SPACE);
        b.visible = true;
        button_box.pack_start (b, false, false, 0);

        var label = new Gtk.Label (text);
        var button = new DialogButton (inactive_filename, active_filename, null, label);
        button.visible = true;

        b.pack_start (button, false, false, 0);
        b.pack_start (label, false, false, 0);

        return button;
    }
}

private class DialogButton : Gtk.Button
{
    private string inactive_filename;
    private string focused_filename;
    private string? active_filename;
    private Gtk.Image i;
    private Gtk.Label? l;

    public DialogButton (string inactive_filename, string focused_filename, string? active_filename, Gtk.Label? label = null)
    {
        this.inactive_filename = inactive_filename;
        this.focused_filename = focused_filename;
        this.active_filename = active_filename;
        relief = Gtk.ReliefStyle.NONE;
        Gtk.button_set_focus_on_click (this, false);
        i = new Gtk.Image.from_file (inactive_filename);
        i.visible = true;
        add (i);

        l = label;

        if (l != null)
        {
            l.visible = true;

            var style_ctx = l.get_style_context();
            try
            {
                var font_provider = new Gtk.CssProvider ();
                var css = "* {font-family: Cantarell; font-size: 12pt;}";
                font_provider.load_from_data (css, -1);
                style_ctx.add_provider (font_provider,
                                        Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
            }
            catch (Error e)
            {
                debug ("Internal error loading font style (Cantarell 12pt): %s", e.message);
            }

            l.override_color (Gtk.StateFlags.NORMAL, { 1.0f, 1.0f, 1.0f, 0.0f });
            l.override_color (Gtk.StateFlags.FOCUSED, { 1.0f, 1.0f, 1.0f, 1.0f });
            l.override_color (Gtk.StateFlags.ACTIVE, { 1.0f, 1.0f, 1.0f, 1.0f });
            this.get_accessible ().set_name (l.get_text ());
        }

        ArcticaGreeter.add_style_class (this);
        try
        {
            // Remove the default GtkButton paddings and border
            var style = new Gtk.CssProvider ();
            style.load_from_data ("* {padding: 0px 0px 0px 0px; border: 0px; }", -1);
            get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
        }
        catch (Error e)
        {
            debug ("Internal error loading session chooser style: %s", e.message);
        }
    }

    public override bool enter_notify_event (Gdk.EventCrossing event)
    {
        grab_focus ();
        return base.enter_notify_event (event);
    }

    public override bool leave_notify_event (Gdk.EventCrossing event)
    {
        (get_toplevel () as Gtk.Window).set_focus (null);
        return base.leave_notify_event (event);
    }

    public override bool draw (Cairo.Context c)
    {
        i.draw (c);
        return true;
    }

    public override void state_flags_changed (Gtk.StateFlags previous_state)
    {
        var new_flags = get_state_flags ();

        if ((new_flags & Gtk.StateFlags.PRELIGHT) != 0 && !can_focus ||
            (new_flags & Gtk.StateFlags.FOCUSED) != 0)
        {
            if ((new_flags & Gtk.StateFlags.ACTIVE) != 0 && active_filename != null)
                i.set_from_file (active_filename);
            else
                i.set_from_file (focused_filename);
        }
        else
        {
            i.set_from_file (inactive_filename);
        }

        if (l != null)
            l.set_state_flags (new_flags, true);

        base.state_flags_changed (previous_state);
    }
}
