/* jshint esnext:true */
/*
 *
 *  GNOME Shell Extension Panel OSD
 *
 * Copyright (C) 2014 - 2015
 *     Jens Lody <jens@jenslody.de>,

 *  Idea: Grab MessageTray OSD widget, and give it new .x and .y co-ordinates.
 *
 *  We're grabbing "private" methods (start with _), so expect this to break
 *  with different versions of Gnome Shell.
 *
 *  It was tested with 3.10 to 3.17.4 with various themes.
 *
 *  Most of this code is a direct copy from gnome-shell/js/ui/messageTray.js,
 *  which is released under GPLv2+.
 *
 *  The idea comes from 'Shell OSD' gnome-shell extension by
 *  mpnordland@gmail.com
 *
 * This file is part of gnome-shell-extension-panel-osd.
 *
 * gnome-shell-extension-panel-osd is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * gnome-shell-extension-panel-osd 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 gnome-shell-extension-panel-osd.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
/*
 */

let versionAtLeast = function(atleast, current) {
    let currentArray = current.split('.');
    let major = currentArray[0];
    let minor = currentArray[1];
    let point = currentArray[2];
    let atleastArray = atleast.split('.');
    if ((atleastArray[0] < major) ||
        (atleastArray[0] == major &&
         atleastArray[1] < minor) ||
        (atleastArray[0] == major &&
         atleastArray[1] == minor) &&
        (atleastArray[2] == undefined ||
         atleastArray[2] <= point))
        return true;
    return false;
}

const ExtensionUtils = imports.misc.extensionUtils;
const Config = imports.misc.config;
const Main = imports.ui.main;
if (versionAtLeast('3.38', Config.PACKAGE_VERSION)) {
   const Tweener = imports.tweener.tweener;
} else{
    const Tweener = imports.ui.tweener;
}
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const St = imports.gi.St;
const Meta = imports.gi.Meta;
const Clutter = imports.gi.Clutter;


const Gettext = imports.gettext.domain('gnome-shell-extension-panel-osd');
const _ = Gettext.gettext;

const PANEL_OSD_SETTINGS_SCHEMA = 'org.gnome.shell.extensions.panel-osd';
const PANEL_OSD_X_POS_KEY = 'x-pos';
const PANEL_OSD_Y_POS_KEY = 'y-pos';
const PANEL_OSD_FORCE_EXPAND = 'force-expand';
const PANEL_OSD_TEST_DELAY = 'test-delay';
const PANEL_OSD_TEST_NOTIFICATION = 'test-notification';

/*
 *  Save MessageTray's original methods.  We're going to change these
 *  in our extension to move the OSD.
 */
let originalExpandMethod;
let originalShowNotification;
let originalUpdateShowingNotification;
let originalHideNotification;

/*
 *  The widget we're interested in
 */
let notificationWidget;
let panel;

/*
 *  We need these constants to call Tween with values consistent to the
 *  MessageTray
 */
const IDLE_TIME = 1000;
const ANIMATION_TIME = 0.2;
const Urgency = {
    LOW: 0,
    NORMAL: 1,
    HIGH: 2,
    CRITICAL: 3
};
const State = {
    HIDDEN: 0,
    SHOWING: 1,
    SHOWN: 2,
    HIDING: 3
};

let _availablePrimaryRect = new Meta.Rectangle({
    x: 0,
    y: 0,
    width: 0,
    height: 0
})

function init() {
    Convenience.initTranslations('gnome-shell-extension-panel-osd');
}

let Settings;
let SettingsC;

let showTestNotificationTimeout;

let loadConfig = function() {
    Settings = Convenience.getSettings(PANEL_OSD_SETTINGS_SCHEMA);
    SettingsC = Settings.connect("changed", function() {
        if (getTestNotification()) {
            if (showTestNotificationTimeout !== undefined)
                Mainloop.source_remove(showTestNotificationTimeout);

            showTestNotificationTimeout = Mainloop.timeout_add(getTestDelay(), Lang.bind(this, function() {
                Main.notify("Panel OSD", _("This is just a multiline test-message to show where the notification will be placed and to test expansion (showing details)."));
                return false;
            }));
            setTestNotification(false);
        }
    });
};


let getX_position = function() {
    if (!Settings)
        loadConfig();
    return Settings.get_double(PANEL_OSD_X_POS_KEY);
};

let getY_position = function() {
    if (!Settings)
        loadConfig();
    return Settings.get_double(PANEL_OSD_Y_POS_KEY);
};

let getForce_expand = function() {
    if (!Settings)
        loadConfig();
    return Settings.get_boolean(PANEL_OSD_FORCE_EXPAND);
};

let getTestDelay = function() {
    if (!Settings)
        loadConfig();
    return Math.floor(1000 * Settings.get_double(PANEL_OSD_TEST_DELAY));
};

let getTestNotification = function() {
    if (!Settings)
        loadConfig();
    return Settings.get_boolean(PANEL_OSD_TEST_NOTIFICATION);
};

let setTestNotification = function(v) {
    if (!Settings)
        loadConfig();
    Settings.set_boolean(PANEL_OSD_TEST_NOTIFICATION, v);
};

let updateAvailablePrimaryRect = function() {
    let monitor=Main.layoutManager.primaryMonitor;
    _availablePrimaryRect.x=monitor.x;
    _availablePrimaryRect.y=monitor.y;
    _availablePrimaryRect.width=monitor.width;
    _availablePrimaryRect.height=monitor.height;

    let panelRect = new Meta.Rectangle({ x: panel.x,
                                         y: panel.y,
                                         width: panel.width,
                                         height: panel.height });
    let [panelIntersects, rect] = _availablePrimaryRect.intersect(panelRect);

    if (panelIntersects)
    {
        if (monitor.width == rect.width)
        {
           _availablePrimaryRect.height = monitor.height - rect.height;
            if (monitor.y == rect.y)
                _availablePrimaryRect.y = rect.height;
        }
        if (monitor.height == rect.height)
        {
           _availablePrimaryRect.width = monitor.width - rect.width;
            if (monitor.x == rect.x)
                _availablePrimaryRect.x = rect.width;
        }
    }
};

/*
 *  Copied from MessageTray._showNotification()
 *
 *  We only change the .y and .x values to move the OSD.  We need to copy
 *  the whole method to prevent the animation from moving the OSD across the
 *  entire screen.
 */
let extensionShowNotification = function() {
    this._notification = this._notificationQueue.shift();
    if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
        this.emit('queue-changed');
    }

    this._userActiveWhileNotificationShown = this.idleMonitor.get_idletime() <= IDLE_TIME;
    if (!this._userActiveWhileNotificationShown) {
        // If the user isn't active, set up a watch to let us know
        // when the user becomes active.
        this.idleMonitor.add_user_active_watch(Lang.bind(this, this._onIdleMonitorBecameActive));
    }

    if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
        this._banner = this._notification.createBanner();
        this._bannerClickedId = this._banner.connect('done-displaying',
                                                     Lang.bind(this, this._escapeTray));
        if (versionAtLeast('3.36', Config.PACKAGE_VERSION)) {
            this._bannerUnfocusedId = this._banner.connect('unfocused', () => {
                this._updateState();
            });

            this._bannerBin.add_actor(this._banner);
        } else
        {
            this._bannerUnfocusedId = this._banner.connect('unfocused', Lang.bind(this, function() {
                this._updateState();
            }));

            this._bannerBin.add_actor(this._banner.actor);
        }

        if (!versionAtLeast('3.34', Config.PACKAGE_VERSION)) {
            this._bannerBin._opacity = 0;
        }
        this._bannerBin.opacity = 0;

        if (versionAtLeast('3.36', Config.PACKAGE_VERSION)) {
            if (getY_position() < 50)
                this._bannerBin.y = Main.layoutManager.monitors[0].height;
            else
                this._bannerBin.y = -this._banner.height;

            this.show();
        } else
        {
            if (getY_position() < 50)
                this._bannerBin.y = Main.layoutManager.monitors[0].height;
            else
                this._bannerBin.y = -this._banner.actor.height;
            this.actor.show();
        }
    } else
    {
        this._notificationClickedId = this._notification.connect('done-displaying',
                                                                 Lang.bind(this, this._escapeTray));
        this._notificationUnfocusedId = this._notification.connect('unfocused', Lang.bind(this, function() {
            this._updateState();
        }));
        this._notificationBin.child = this._notification.actor;

        this._notificationWidget.opacity = 0;
        // JRL changes begin
        //this._notificationWidget.y = 0;
        if (getY_position() < 50)
            this._notificationWidget.y = this.actor.height;
        else
            this._notificationWidget.y = -(Main.layoutManager.monitors[0].y + Main.layoutManager.monitors[0].height);
        // JRL changes end


        this._notificationWidget.show();
    }

    if (versionAtLeast('3.30', Config.PACKAGE_VERSION)) {
        Meta.disable_unredirect_for_display(global.display);
    }
    this._updateShowingNotification();

    let x, y, mods;
    if (versionAtLeast('3.36', Config.PACKAGE_VERSION)) {
        [x, y] = global.get_pointer();
    } else
    {
        [x, y, mods] = global.get_pointer();
    }
    // We save the position of the mouse at the time when we started showing the notification
    // in order to determine if the notification popped up under it. We make that check if
    // the user starts moving the mouse and _onTrayHoverChanged() gets called. We don't
    // expand the notification if it just happened to pop up under the mouse unless the user
    // explicitly mouses away from it and then mouses back in.
    this._showNotificationMouseX = x;
    this._showNotificationMouseY = y;
    // We save the coordinates of the mouse at the time when we started showing the notification
    // and then we update it in _notificationTimeout(). We don't pop down the notification if
    // the mouse is moving towards it or within it.
    this._lastSeenMouseX = x;
    this._lastSeenMouseY = y;
    if (versionAtLeast('3.14', Config.PACKAGE_VERSION)) {
        this._resetNotificationLeftTimeout();
    }
};


/*
 *  Copied from MessageTray._hideNotification()
 *
 *  We only change the .y and .x values to move the OSD.  We need to copy
 *  the whole method to prevent the animation from moving the OSD across the
 *  entire screen.
 */
let extensionHideNotification = function(animate) {
    this._notificationFocusGrabber.ungrabFocus();

    let yPos;
    if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
        if (getY_position() < 50)
            yPos = Main.layoutManager.monitors[0].y + Main.layoutManager.monitors[0].height;
        else
            yPos = -this._bannerBin.height;

        if (this._bannerClickedId) {
            this._banner.disconnect(this._bannerClickedId);
            this._bannerClickedId = 0;
        }
        if (this._bannerUnfocusedId) {
            this._banner.disconnect(this._bannerUnfocusedId);
            this._bannerUnfocusedId = 0;
        }

    }else
    {
        if (this._notificationExpandedId) {
            this._notification.disconnect(this._notificationExpandedId);
            this._notificationExpandedId = 0;
        }
        // JRL changes begin
        if (getY_position() < 50)
            yPos = this.actor.height;
        else
            yPos = -(Main.layoutManager.monitors[0].y + Main.layoutManager.monitors[0].height);
        // JRL changes end
        if (this._notificationClickedId) {
            this._notification.disconnect(this._notificationClickedId);
            this._notificationClickedId = 0;
        }
        if (this._notificationUnfocusedId) {
            this._notification.disconnect(this._notificationUnfocusedId);
            this._notificationUnfocusedId = 0;
        }
    }

    if (versionAtLeast('3.14', Config.PACKAGE_VERSION)) {
        this._resetNotificationLeftTimeout();
    }
    else
    {
        if (this._notificationLeftTimeoutId) {
            Mainloop.source_remove(this._notificationLeftTimeoutId);
            this._notificationLeftTimeoutId = 0;
            this._notificationLeftMouseX = -1;
            this._notificationLeftMouseY = -1;
        }
    }

    if (versionAtLeast('3.34', Config.PACKAGE_VERSION)) {
        this._bannerBin.remove_all_transitions();

        if (animate) {
            this._notificationState = State.HIDING;
            this._bannerBin.ease({
                opacity: 0,
                duration: ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_BACK
            });
            this._bannerBin.ease({
                // JRL changes begin
                y: yPos,
                // JRL changes end
                duration: ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_BACK,
                onComplete: () => {
                    this._notificationState = State.HIDDEN;
                    this._hideNotificationCompleted();
                    this._updateState();
                }
            });
        } else {
            // JRL changes begin
            this._bannerBin.y = yPos;
            // JRL changes end
            this._bannerBin.opacity = 0;
            this._notificationState = State.HIDDEN;
            this._hideNotificationCompleted();
        }
    }
    else
    {
        // JRL changes begin
        let theNotification;
        if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
            theNotification = this._bannerBin;
        }else
        {
            theNotification = this._notificationWidget;
        }
       // JRL changes end


        if (animate) {
            if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
                // JRL changes begin
                this._tween(theNotification, '_notificationState', State.HIDDEN,
                            { y: yPos,
                            // JRL changes end
                              _opacity: 0,
                              time: ANIMATION_TIME,
                              transition: 'easeOutBack',
                              onUpdate: this._clampOpacity,
                              onUpdateScope: this,
                              onComplete: this._hideNotificationCompleted,
                              onCompleteScope: this
                            });
            } else {
                // JRL changes begin
                this._tween(theNotification, '_notificationState', State.HIDDEN,
                            { y: yPos,
                            // JRL changes end
                              opacity: 0,
                              time: ANIMATION_TIME,
                              transition: 'easeOutQuad',
                              onComplete: this._hideNotificationCompleted,
                              onCompleteScope: this
                            });
            }
        } else {
            // JRL changes begin
            Tweener.removeTweens(theNotification);
            theNotification.y = yPos;
            theNotification.opacity = 0;
            // JRL changes end
            this._notificationState = State.HIDDEN;
            this._hideNotificationCompleted();
        }
    }
};


/*
 *  Copied from MessageTray._updateShowingNotification()
 *
 *  We only change the .y and .x values to move the OSD.  We need to copy
 *  the whole method to prevent the animation from moving the OSD across the
 *  entire screen.
 *
 */
let extensionUpdateShowingNotification = function() {
    // JRL changes begin
    // first reset the border-radius to the default
    if (!versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
        this._notification._table.set_style('border-radius:;');
        if (getY_position() > 0.1)
        {
            // fix the border-radiuses, depending on the position
            let tl, tr;
            let bl = this._notification._table.get_theme_node().get_border_radius(St.Corner.TOPLEFT);
            let br = this._notification._table.get_theme_node().get_border_radius(St.Corner.TOPRIGHT);
            if (getY_position() >= 99.9)
            {
                tl = this._notification._table.get_theme_node().get_border_radius(St.Corner.BOTTOMLEFT);
                tr = this._notification._table.get_theme_node().get_border_radius(St.Corner.BOTTOMRIGHT);
            }
            else
            {
                tl = bl;
                tr = br;
            }
            this._notification._table.set_style('border-radius: '+tl+'px '+tr+'px '+bl+'px '+br+'px;');
        }
    }
    // JRL changes end
    this._notification.acknowledged = true;
    this._notification.playSound();
    // We auto-expand notifications with CRITICAL urgency, or for which the relevant setting
    // is on in the control center.
    if (this._notification.urgency == Urgency.CRITICAL ||
        // JRL changes begin
        getForce_expand() ||
        // JRL changes end
        this._notification.source.policy.forceExpanded)
        {
            if (versionAtLeast('3.16', Config.PACKAGE_VERSION))
                this._expandBanner(true);
            else
                this._expandNotification(true);
        }


    // JRL changes begin
    let theNotification;
    if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
        theNotification = this._bannerBin;
    }else
    {
        theNotification =this._notificationWidget;
    }
    // use panel's y and height property to determine the bottom of the top-panel.
    // needed because the "hide top bar" and "hide top panel" use different approaches to hide the
    // top bar.
    // "hide top panel" keeps the height and just moves the panel out of the visible area, so using
    // the panels-height is not enough.
    let yPos;
    updateAvailablePrimaryRect();
    if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {

        yPos = (_availablePrimaryRect.height - theNotification.height) * (100-getY_position()) / 100;

    }else
    {
        let yTop = -(Main.layoutManager.bottomMonitor.y + Main.layoutManager.bottomMonitor.height);
        if (Main.layoutManager.bottomMonitor == Main.layoutManager.primaryMonitor)
            yTop += (panel.y + panel.height);
        if (yTop < (-Main.layoutManager.bottomMonitor.height))
            yTop = -Main.layoutManager.bottomMonitor.height;
        let yBottom = -theNotification.height;

        yPos = (yTop - yBottom) * getY_position() / 100 + yBottom;
        //
    }
    theNotification.x = (_availablePrimaryRect.width - theNotification.width) * (getX_position() - 50) / 50;
    // JRL changes end
    // We tween all notifications to full opacity. This ensures that both new notifications and
    // notifications that might have been in the process of hiding get full opacity.
    //
    // We tween any notification showing in the banner mode to the appropriate height
    // (which is banner height or expanded height, depending on the notification state)
    // This ensures that both new notifications and notifications in the banner mode that might
    // have been in the process of hiding are shown with the correct height.
    //
    // We use this._showNotificationCompleted() onComplete callback to extend the time the updated
    // notification is being shown.

    if (versionAtLeast('3.34', Config.PACKAGE_VERSION)) {
        this._notificationState = State.SHOWING;
        this._bannerBin.remove_all_transitions();
        this._bannerBin.ease({
            opacity: 255,
            duration: ANIMATION_TIME,
            mode: Clutter.AnimationMode.LINEAR
        });
        this._bannerBin.ease({
            // JRL changes begin
            y: yPos,
            // JRL changes end
            duration: ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_BACK,
            onComplete: () => {
                this._notificationState = State.SHOWN;
                this._showNotificationCompleted();
                this._updateState();
            }
        });
    }
    else
    {
        let tweenParams;
        if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
            tweenParams = { _opacity: 255,
                                // JRL changes begin
                                y: yPos,
                                // JRL changes end
                                time: ANIMATION_TIME,
                                transition: 'easeOutBack',
                                onUpdate: this._clampOpacity,
                                onUpdateScope: this,
                                onComplete: this._showNotificationCompleted,
                                onCompleteScope: this
                              };
        }else
        {
            tweenParams = { opacity: 255,
                                // JRL changes begin
                                y: yPos,
                                // JRL changes end
                                time: ANIMATION_TIME,
                                transition: 'easeOutQuad',
                                onComplete: this._showNotificationCompleted,
                                onCompleteScope: this
                              };
        }

        this._tween(theNotification, '_notificationState', State.SHOWN, tweenParams);
    }
};

/*
 *  Copied from MessageTray._onNotificationExpanded()
 *
 *  We only change the .y and .x values to move the OSD.  We need to copy
 *  the whole method to prevent the animation from moving the OSD across the
 *  entire screen.
 *
 */
let extensiononNotificationExpanded = function() {
    // JRL changes begin
    //let expandedY = - this._notificationWidget.height;
    let yTop = -(Main.layoutManager.monitors[0].y + Main.layoutManager.monitors[0].height);
    if (Main.layoutManager.monitors[0] == Main.layoutManager.primaryMonitor)
        yTop += (panel.y + panel.height);
    if (yTop < (-Main.layoutManager.monitors[0].height))
        yTop = -Main.layoutManager.monitors[0].height;
    let yBottom = -this._notificationWidget.height;

    let expandedY = (yTop - yBottom) * getY_position() / 100 + yBottom;
    // JRL changes end
    this._closeButton.show();

    // Don't animate the notification to its new position if it has shrunk:
    // there will be a very visible "gap" that breaks the illusion.
    if (this._notificationWidget.y < expandedY) {
        this._notificationWidget.y = expandedY;
    } else if (this._notification.y != expandedY) {
        // Tween also opacity here, to override a possible tween that's
        // currently hiding the notification.
        if (versionAtLeast('3.14', Config.PACKAGE_VERSION)) {
            Tweener.addTween(this._notificationWidget,
                             { y: expandedY,
                               opacity: 255,
                               time: ANIMATION_TIME,
                               transition: 'easeOutQuad',
                               // HACK: Drive the state machine here better,
                               // instead of overwriting tweens
                               onComplete: Lang.bind(this, function() {
                                   this._notificationState = State.SHOWN;
                               }),
                             });
        }
        else
        {
            this._tween(this._notificationWidget, '_notificationState', State.SHOWN,
                        { y: expandedY,
                          opacity: 255,
                          time: ANIMATION_TIME,
                          transition: 'easeOutQuad'
                        });
        }
    }
};

/*
 *  Overload the methods.
 */
function enable() {
    if (versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
        notificationWidget = Main.messageTray._bannerBin;
    }else
    {
        notificationWidget = Main.messageTray._notificationWidget;
        originalExpandMethod = Main.messageTray._onNotificationExpanded;
        Main.messageTray._onNotificationExpanded = extensiononNotificationExpanded;
    }
    originalShowNotification = Main.messageTray._showNotification;
    Main.messageTray._showNotification = extensionShowNotification;

    originalUpdateShowingNotification = Main.messageTray._updateShowingNotification;
    Main.messageTray._updateShowingNotification = extensionUpdateShowingNotification;

    originalHideNotification = Main.messageTray._hideNotification;
    Main.messageTray._hideNotification = extensionHideNotification;

    panel = Main.layoutManager.panelBox;

    loadConfig();
}


/*
 *  Put everything back.
 */
function disable() {
    if (SettingsC) {
        Settings.disconnect(SettingsC);
        SettingsC = undefined;
    }

    if (showTestNotificationTimeout !== undefined)
        Mainloop.source_remove(showTestNotificationTimeout);

    // reset x-position
    notificationWidget.x = 0;

    Main.messageTray._showNotification = originalShowNotification;
    Main.messageTray._hideNotification = originalHideNotification;
    Main.messageTray._updateShowingNotification = originalUpdateShowingNotification;
    if (!versionAtLeast('3.16', Config.PACKAGE_VERSION)) {
        // remove our (inline-)style, in case we just show a notification, otherwise the radius is drawn incorrect
        if (Main.messageTray._notification)
            Main.messageTray._notification._table.set_style('border-radius:;');
        Main.messageTray._onNotificationExpanded = originalExpandMethod;
    }
}
