/*
 * This file is part of the Dash-To-Panel extension for Gnome 3
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * 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/>.
 *
 * Credits:
 * This file is based on code from the Dash to Dock extension by micheleg
 * and code from the Taskbar extension by Zorin OS
 * 
 * Code to re-anchor the panel was taken from Thoma5 BottomPanel:
 * https://github.com/Thoma5/gnome-shell-extension-bottompanel
 * 
 * Pattern for moving clock based on Frippery Move Clock by R M Yorston
 * http://frippery.org/extensions/
 * 
 * Some code was also adapted from the upstream Gnome Shell source code.
 */

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import * as AppIcons from './appIcons.js';
import * as Utils from './utils.js';
import * as Taskbar from './taskbar.js';
import * as TaskbarItemContainer from './taskbar.js';
import * as Pos from './panelPositions.js';
import * as PanelSettings from './panelSettings.js';
import * as PanelStyle from './panelStyle.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Dash from 'resource:///org/gnome/shell/ui/dash.js';
import * as CtrlAltTab from 'resource:///org/gnome/shell/ui/ctrlAltTab.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import St from 'gi://St';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
import Shell from 'gi://Shell';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as DateMenu from 'resource:///org/gnome/shell/ui/dateMenu.js';
import * as Volume from 'resource:///org/gnome/shell/ui/status/volume.js';
import * as Progress from './progress.js';

import * as Intellihide from './intellihide.js';
import * as Transparency from './transparency.js';
import {SETTINGS, DESKTOPSETTINGS, PERSISTENTSTORAGE} from './extension.js';
import {gettext as _, InjectionManager} from 'resource:///org/gnome/shell/extensions/extension.js';

let tracker = Shell.WindowTracker.get_default();
export const panelBoxes = ['_leftBox', '_centerBox', '_rightBox'];

//timeout names
const T2 = 'startIntellihideTimeout';
const T4 = 'showDesktopTimeout';
const T5 = 'trackerFocusAppTimeout';
const T6 = 'scrollPanelDelayTimeout';
const T7 = 'waitPanelBoxAllocation';

export const Panel = GObject.registerClass({
}, class Panel extends St.Widget {

    _init(panelManager, monitor, panelBox, isStandalone) {
        super._init({ layout_manager: new Clutter.BinLayout() });

        this._timeoutsHandler = new Utils.TimeoutsHandler();
        this._signalsHandler = new Utils.GlobalSignalsHandler();
        this._injectionManager = new InjectionManager();

        this.panelManager = panelManager;
        this.panelStyle = new PanelStyle.PanelStyle();

        this.monitor = monitor;
        this.panelBox = panelBox;

        // when the original gnome-shell top panel is kept, all panels are "standalone",
        // so in this case use isPrimary to get the panel on the primary dtp monitor, which
        // might be different from the system's primary monitor.
        this.isStandalone = isStandalone;
        this.isPrimary = !isStandalone || (SETTINGS.get_boolean('stockgs-keep-top-panel') && 
                                           monitor == panelManager.dtpPrimaryMonitor);

        this._sessionStyle = null;
        this._unmappedButtons = [];
        this._elementGroups = [];

        let systemMenuInfo = Utils.getSystemMenuInfo();

        if (isStandalone) {
            this.panel = new SecondaryPanel({ name: 'panel', reactive: true });
            this.statusArea = this.panel.statusArea = {};

            //next 3 functions are needed by other extensions to add elements to the secondary panel
            this.panel.addToStatusArea = function(role, indicator, position, box) {
                return Main.panel.addToStatusArea.call(this, role, indicator, position, box);
            };

            this.panel._addToPanelBox = function(role, indicator, position, box) {
                Main.panel._addToPanelBox.call(this, role, indicator, position, box);
            };

            this.panel._onMenuSet = function(indicator) {
                Main.panel._onMenuSet.call(this, indicator);
            };

            this._leftBox = this.panel._leftBox = new St.BoxLayout({ name: 'panelLeft' });
            this._centerBox = this.panel._centerBox = new St.BoxLayout({ name: 'panelCenter' });
            this._rightBox = this.panel._rightBox = new St.BoxLayout({ name: 'panelRight' });

            this.menuManager = this.panel.menuManager = new PopupMenu.PopupMenuManager(this.panel);

            this._setPanelMenu(systemMenuInfo.name, systemMenuInfo.constructor, this.panel);
            this._setPanelMenu('dateMenu', DateMenu.DateMenuButton, this.panel);
            this._setPanelMenu('activities', Main.panel.statusArea.activities.constructor, this.panel);

            this.panel.add_child(this._leftBox);
            this.panel.add_child(this._centerBox);
            this.panel.add_child(this._rightBox);
        } else {
            this.panel = Main.panel;
            this.statusArea = Main.panel.statusArea;
            this.menuManager = Main.panel.menuManager;

            panelBoxes.forEach(p => this[p] = Main.panel[p]);

            ['activities', systemMenuInfo.name, 'dateMenu'].forEach(b => {
                let container = this.statusArea[b].container;
                let parent = container.get_parent();

                container._dtpOriginalParent = parent;
                parent ? parent.remove_child(container) : null;
                this.panel.add_child(container);
            });
        }

        // Create a wrapper around the real showAppsIcon in order to add a popupMenu. Most of 
        // its behavior is handled by the taskbar, but its positioning is done at the panel level
        this.showAppsIconWrapper = new AppIcons.ShowAppsIconWrapper(this);
        this.panel.add_child(this.showAppsIconWrapper.realShowAppsIcon);

        this.panel._delegate = this;
        
        this.add_child(this.panel);

        if (Main.panel._onButtonPress || Main.panel._tryDragWindow) {
            this._signalsHandler.add([
                this.panel, 
                [
                    'button-press-event', 
                    'touch-event'
                ],
                this._onButtonPress.bind(this)
            ]);
        }

        if (Main.panel._onKeyPress) {
            this._signalsHandler.add([this.panel, 'key-press-event', Main.panel._onKeyPress.bind(this)]);
        }
       
        Main.ctrlAltTabManager.addGroup(this, _("Top Bar")+" "+ monitor.index, 'focus-top-bar-symbolic',
                                        { sortGroup: CtrlAltTab.SortGroup.TOP });
    }

    enable () {
        let { name: systemMenuName } = Utils.getSystemMenuInfo();

        if (this.statusArea[systemMenuName] && this.statusArea[systemMenuName]._volumeOutput) {
            Utils.getIndicators(this.statusArea[systemMenuName]._volumeOutput)._dtpIgnoreScroll = 1;
        }

        this.geom = this.getGeometry();
        
        this._setPanelPosition();

        if (!this.isStandalone) {
            this._injectionManager.overrideMethod(Object.getPrototypeOf(this.panel), 'vfunc_allocate', () => (box) => this._mainPanelAllocate(box));

            // remove the extra space before the clock when the message-indicator is displayed
            if (DateMenu.IndicatorPad) {
                this._injectionManager.overrideMethod(DateMenu.IndicatorPad.prototype, 'vfunc_get_preferred_width', () => () => [0,0]);
                this._injectionManager.overrideMethod(DateMenu.IndicatorPad.prototype, 'vfunc_get_preferred_height', () => () => [0,0]);
            }
        }

        if (!DateMenu.IndicatorPad && this.statusArea.dateMenu) {
            //3.36 switched to a size constraint applied on an anonymous child
            let indicatorPad = this.statusArea.dateMenu.get_first_child().get_first_child();

            this._dateMenuIndicatorPadContraints = indicatorPad.get_constraints();
            indicatorPad.clear_constraints();
        }

        this.menuManager._oldChangeMenu = this.menuManager._changeMenu;
        this.menuManager._changeMenu = (menu) => {
            if (!SETTINGS.get_boolean('stockgs-panelbtn-click-only')) {
                this.menuManager._oldChangeMenu(menu);
            }
        };

        this.dynamicTransparency = new Transparency.DynamicTransparency(this);
        
        this.taskbar = new Taskbar.Taskbar(this);

        this.panel.add_child(this.taskbar.actor);

        this._setShowDesktopButton(true);
        
        this._setAllocationMap();

        this.panel.add_style_class_name('dashtopanelMainPanel ' + this.getOrientation());

        this._timeoutsHandler.add([T2, SETTINGS.get_int('intellihide-enable-start-delay'), () => this.intellihide = new Intellihide.Intellihide(this)]);

        this._signalsHandler.add(
            // this is to catch changes to the theme or window scale factor
            [
                Utils.getStageTheme(), 
                'changed', 
                () => (this._resetGeometry(), this._setShowDesktopButtonStyle()),
            ],
            [
                // sync hover after a popupmenu is closed
                this.taskbar,
                'menu-closed', 
                () => this.panel.sync_hover()
            ],
            [
                Main.overview,
                [
                    'showing',
                    'hiding'
                ],
                () => this._adjustForOverview()
            ],
            [
                Main.overview,
                'hidden',
                () => {
                    if (this.isPrimary) {
                        //reset the primary monitor when exiting the overview
                        this.panelManager.setFocusedMonitor(this.monitor);
                    }
                }
            ],
            [
                this.statusArea.activities,
                'captured-event', 
                (actor, e) => {
                    if (e.type() == Clutter.EventType.BUTTON_PRESS || e.type() == Clutter.EventType.TOUCH_BEGIN) {
                        //temporarily use as primary the monitor on which the activities btn was clicked 
                        this.panelManager.setFocusedMonitor(this.monitor);
                    }
                }
            ],
            [
                this._centerBox,
                'actor-added',
                () => this._onBoxActorAdded(this._centerBox)
            ],
            [
                this._rightBox,
                'actor-added',
                () => this._onBoxActorAdded(this._rightBox)
            ],
            [
                this.panel,
                'scroll-event',
                this._onPanelMouseScroll.bind(this)
            ],
            [
                Main.layoutManager,
                'startup-complete',
                () => this._resetGeometry()
            ]
        );

        this._bindSettingsChanges();

        this.panelStyle.enable(this);

        if (this.checkIfVertical()) {
            this._signalsHandler.add([
                this.panelBox,
                'notify::visible',
                () => {
                    if (this.panelBox.visible) {
                        this._refreshVerticalAlloc();
                    }
                }
            ]);

            if (this.statusArea.dateMenu) {
                this._formatVerticalClock();
                
                this._signalsHandler.add([
                    this.statusArea.dateMenu._clock,
                    'notify::clock',
                    () => this._formatVerticalClock()
                ]);
            }
        }

        // Since we are usually visible but not usually changing, make sure
        // most repaint requests don't actually require us to repaint anything.
        // This saves significant CPU when repainting the screen.
        this.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);

        this._initProgressManager();
    }

    disable() {
        this.panelStyle.disable();

        this._timeoutsHandler.destroy();
        this._signalsHandler.destroy();
        
        this.panel.remove_child(this.taskbar.actor);

        if (this.intellihide) {
            this.intellihide.destroy();
        }

        this.dynamicTransparency.destroy();

        this.progressManager.destroy();

        this.taskbar.destroy();
        this.showAppsIconWrapper.destroy();

        this.menuManager._changeMenu = this.menuManager._oldChangeMenu;

        this._unmappedButtons.forEach(a => this._disconnectVisibleId(a));

        if (this.statusArea.dateMenu) {
            this.statusArea.dateMenu._clockDisplay.text = this.statusArea.dateMenu._clock.clock;
            this.statusArea.dateMenu._clockDisplay.clutter_text.set_width(-1);

            if (this._dateMenuIndicatorPadContraints) {
                let indicatorPad = this.statusArea.dateMenu.get_first_child().get_first_child();

                this._dateMenuIndicatorPadContraints.forEach(c => indicatorPad.add_constraint(c));
            }
        }

        this._setVertical(this.panel, false);
        this._setVertical(this._centerBox, false);
        this._setVertical(this._rightBox, false);

        let { name: systemMenuName } = Utils.getSystemMenuInfo();

        if (!this.isStandalone) {
            ['vertical', 'horizontal', 'dashtopanelMainPanel'].forEach(c => this.panel.remove_style_class_name(c));

            if (!Main.sessionMode.isLocked) {
                [['activities', 0], [systemMenuName, -1], ['dateMenu', 0]].forEach(b => {
                    let container = this.statusArea[b[0]].container;
                    let originalParent = container._dtpOriginalParent;
    
                    this.panel.remove_child(container);
                    originalParent ? originalParent.insert_child_at_index(container, b[1]) : null;
                    delete container._dtpOriginalParent;
                });
            }

            this._setShowDesktopButton(false);

            delete Utils.getIndicators(this.statusArea[systemMenuName]._volumeOutput)._dtpIgnoreScroll;

            this._injectionManager.clear();
            
            this.panel._delegate = this.panel;
        } else {
            this._removePanelMenu('dateMenu');
            this._removePanelMenu(systemMenuName);
            this._removePanelMenu('activities');
        }

        Main.ctrlAltTabManager.removeGroup(this);
    }

    handleDragOver(source, actor, x, y, time) {
        if (source == Main.xdndHandler) {
            
            // open overview so they can choose a window for focusing
            // and ultimately dropping dragged item onto
            if(Main.overview.shouldToggleByCornerOrButton())
                Main.overview.show();
        }
        
        return DND.DragMotionResult.CONTINUE;
    }

    getPosition() {
        let position = PanelSettings.getPanelPosition(SETTINGS, this.monitor.index);

        if (position == Pos.TOP) {
            return St.Side.TOP;
        } else if (position == Pos.RIGHT) {
            return St.Side.RIGHT;
        } else if (position == Pos.BOTTOM) {
            return St.Side.BOTTOM;
        }
        
        return St.Side.LEFT;
    }

    checkIfVertical() {
        let position = this.getPosition();
    
        return (position == St.Side.LEFT || position == St.Side.RIGHT);
    }
    
    getOrientation() {
        return (this.checkIfVertical() ? 'vertical' : 'horizontal');
    }

    updateElementPositions() {
        let panelPositions = this.panelManager.panelsElementPositions[this.monitor.index] || Pos.defaults;

        this._updateGroupedElements(panelPositions);

        this.panel.hide();
        this.panel.show();
    }

    _updateGroupedElements(panelPositions) {
        let previousPosition = 0;
        let previousCenteredPosition = 0;
        let currentGroup = -1;

        this._elementGroups = [];

        panelPositions.forEach(pos => {
            let allocationMap = this.allocationMap[pos.element];

            if (allocationMap.actor) {
                allocationMap.actor.visible = pos.visible;

                if (!pos.visible) {
                    return;
                }

                let currentPosition = pos.position;
                let isCentered = Pos.checkIfCentered(currentPosition);

                if (currentPosition == Pos.STACKED_TL && previousPosition == Pos.STACKED_BR) {
                    currentPosition = Pos.STACKED_BR;
                }

                if (!previousPosition || 
                    (previousPosition == Pos.STACKED_TL && currentPosition != Pos.STACKED_TL) ||
                    (previousPosition != Pos.STACKED_BR && currentPosition == Pos.STACKED_BR) ||
                    (isCentered && previousPosition != currentPosition && previousPosition != Pos.STACKED_BR)) {
                    this._elementGroups[++currentGroup] = { elements: [], index: this._elementGroups.length, expandableIndex: -1 };
                    previousCenteredPosition = 0;
                }

                if (pos.element == Pos.TASKBAR) {
                    this._elementGroups[currentGroup].expandableIndex = this._elementGroups[currentGroup].elements.length;
                }

                if (isCentered && !this._elementGroups[currentGroup].isCentered) {
                    this._elementGroups[currentGroup].isCentered = 1;
                    previousCenteredPosition = currentPosition;
                }

                this._elementGroups[currentGroup].position = previousCenteredPosition || currentPosition;
                this._elementGroups[currentGroup].elements.push(allocationMap);

                allocationMap.position = currentPosition;
                previousPosition = currentPosition;
            }
        });
    }

    _bindSettingsChanges() {
        let isVertical = this.checkIfVertical();

        this._signalsHandler.add(
            [
                SETTINGS,
                [
                    'changed::panel-sizes',
                    'changed::group-apps'
                ],
                () => this._resetGeometry()
            ],
            [
                SETTINGS,
                [
                    'changed::appicon-margin',
                    'changed::appicon-padding'
                ],
                () => this.taskbar.resetAppIcons()
            ],
            [
                SETTINGS,
                [
                    'changed::showdesktop-button-width',
                    'changed::trans-use-custom-bg',
                    'changed::desktop-line-use-custom-color',
                    'changed::desktop-line-custom-color',
                    'changed::trans-bg-color'
                ],
                () => this._setShowDesktopButtonStyle()
            ],
            [
                DESKTOPSETTINGS,
                'changed::clock-format',
                () => {
                    this._clockFormat = null;
                    
                    if (isVertical) {
                        this._formatVerticalClock();
                    }
                }
            ],
            [
                SETTINGS,
                'changed::progress-show-bar',
                () => this._initProgressManager()
            ],
            [
                SETTINGS,
                'changed::progress-show-count',
                () => this._initProgressManager()
            ]
        );

        if (isVertical) {
            this._signalsHandler.add([SETTINGS, 'changed::group-apps-label-max-width', () => this._resetGeometry()]);
        }
    }

    _setPanelMenu(propName, constr, container) {
        if (!this.statusArea[propName]) {
            this.statusArea[propName] = this._getPanelMenu(propName, constr);
            this.menuManager.addMenu(this.statusArea[propName].menu);
            container.insert_child_at_index(this.statusArea[propName].container, 0);
        }
    }
    
    _removePanelMenu(propName) {
        if (this.statusArea[propName]) {
            let parent = this.statusArea[propName].container.get_parent();

            if (parent) {
                parent.remove_actor(this.statusArea[propName].container);
            }

            //calling this.statusArea[propName].destroy(); is buggy for now, gnome-shell never
            //destroys those panel menus...
            //since we can't destroy the menu (hence properly disconnect its signals), let's 
            //store it so the next time a panel needs one of its kind, we can reuse it instead 
            //of creating a new one
            let panelMenu = this.statusArea[propName];

            this.menuManager.removeMenu(panelMenu.menu);
            PERSISTENTSTORAGE[propName].push(panelMenu);
            this.statusArea[propName] = null;
        }
    }

    _getPanelMenu(propName, constr) {
        PERSISTENTSTORAGE[propName] = PERSISTENTSTORAGE[propName] || [];

        if (!PERSISTENTSTORAGE[propName].length) {
            PERSISTENTSTORAGE[propName].push(new constr());
        }

        return PERSISTENTSTORAGE[propName].pop();
    }

    _adjustForOverview() {
        let isFocusedMonitor = this.panelManager.checkIfFocusedMonitor(this.monitor);
        let isOverview = !!Main.overview.visibleTarget;
        let isOverviewFocusedMonitor = isOverview && isFocusedMonitor;
        let isShown = !isOverview || isOverviewFocusedMonitor;
        let actorData = Utils.getTrackedActorData(this.panelBox)

        // prevent the "chrome" to update the panelbox visibility while in overview
        actorData.trackFullscreen = !isOverview

        this.panelBox[isShown ? 'show' : 'hide']();
    }

    _resetGeometry() {
        this.geom = this.getGeometry();
        this._setPanelPosition();
        this.taskbar.resetAppIcons(true);
        this.dynamicTransparency.updateExternalStyle();

        if (this.intellihide && this.intellihide.enabled) {
            this.intellihide.reset();
        }

        if (this.checkIfVertical()) {
            this.showAppsIconWrapper.realShowAppsIcon.toggleButton.set_width(this.geom.w);
            this._refreshVerticalAlloc();
        }
    }

    getGeometry() {
        let scaleFactor = Utils.getScaleFactor();
        let panelBoxTheme = this.panelBox.get_theme_node();
        let lrPadding = panelBoxTheme.get_padding(St.Side.RIGHT) + panelBoxTheme.get_padding(St.Side.LEFT);
        let topPadding = panelBoxTheme.get_padding(St.Side.TOP);
        let tbPadding = topPadding + panelBoxTheme.get_padding(St.Side.BOTTOM);
        let position = this.getPosition();
        let length = PanelSettings.getPanelLength(SETTINGS, this.monitor.index) / 100;
        let anchor = PanelSettings.getPanelAnchor(SETTINGS, this.monitor.index);
        let anchorPlaceOnMonitor = 0;
        let gsTopPanelOffset = 0;
        let x = 0, y = 0;
        let w = 0, h = 0;

        const panelSize = PanelSettings.getPanelSize(SETTINGS, this.monitor.index);
        this.dtpSize = panelSize * scaleFactor;

        if (SETTINGS.get_boolean('stockgs-keep-top-panel') && Main.layoutManager.primaryMonitor == this.monitor) {
            gsTopPanelOffset = Main.layoutManager.panelBox.height - topPadding;
        }

        if (this.checkIfVertical()) {
            if (!SETTINGS.get_boolean('group-apps')) {
                // add window title width and side padding of _dtpIconContainer when vertical
                this.dtpSize += SETTINGS.get_int('group-apps-label-max-width') + AppIcons.DEFAULT_PADDING_SIZE * 2 / scaleFactor;
            }

            this.sizeFunc = 'get_preferred_height',
            this.fixedCoord = { c1: 'x1', c2: 'x2' }
            this.varCoord = { c1: 'y1', c2: 'y2' };

            w = this.dtpSize;
            h = this.monitor.height * length - tbPadding - gsTopPanelOffset;
        } else {
            this.sizeFunc = 'get_preferred_width';
            this.fixedCoord = { c1: 'y1', c2: 'y2' };
            this.varCoord = { c1: 'x1', c2: 'x2' };

            w = this.monitor.width * length - lrPadding;
            h = this.dtpSize;
        }

        if (position == St.Side.TOP || position == St.Side.LEFT) {
            x = this.monitor.x;
            y = this.monitor.y + gsTopPanelOffset;
        } else if (position == St.Side.RIGHT) {
            x = this.monitor.x + this.monitor.width - this.dtpSize - lrPadding;
            y = this.monitor.y + gsTopPanelOffset;
        } else { //BOTTOM
            x = this.monitor.x;
            y = this.monitor.y + this.monitor.height - this.dtpSize - tbPadding;
        }

        if (this.checkIfVertical()) {
            let viewHeight = this.monitor.height - gsTopPanelOffset;
            
            if (anchor === Pos.MIDDLE) {
                anchorPlaceOnMonitor = (viewHeight - h) / 2;
            } else if (anchor === Pos.END) {
                anchorPlaceOnMonitor = viewHeight - h;
            } else { // Pos.START
                anchorPlaceOnMonitor = 0;
            }
            y = y + anchorPlaceOnMonitor;
        } else {
            if (anchor === Pos.MIDDLE) {
                anchorPlaceOnMonitor = (this.monitor.width - w) / 2;
            } else if (anchor === Pos.END) {
                anchorPlaceOnMonitor = this.monitor.width - w;
            } else { // Pos.START
                anchorPlaceOnMonitor = 0;
            }
            x = x + anchorPlaceOnMonitor;
        }

        return {
            x, y, 
            w, h,
            lrPadding,
            tbPadding,
            position
        };
    }

    _setAllocationMap() {
        this.allocationMap = {};
        let setMap = (name, actor) => this.allocationMap[name] = { 
            actor: actor,
            box: new Clutter.ActorBox() 
        };
        
        setMap(Pos.SHOW_APPS_BTN, this.showAppsIconWrapper.realShowAppsIcon);
        setMap(Pos.ACTIVITIES_BTN, this.statusArea.activities ? this.statusArea.activities.container : 0);
        setMap(Pos.LEFT_BOX, this._leftBox);
        setMap(Pos.TASKBAR, this.taskbar.actor);
        setMap(Pos.CENTER_BOX, this._centerBox);
        setMap(Pos.DATE_MENU, this.statusArea.dateMenu.container);
        setMap(Pos.SYSTEM_MENU, this.statusArea[Utils.getSystemMenuInfo().name].container);
        setMap(Pos.RIGHT_BOX, this._rightBox);
        setMap(Pos.DESKTOP_BTN, this._showDesktopButton);
    }

    _mainPanelAllocate(box) {
        this.panel.set_allocation(box);
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        let fixed = 0;
        let centeredMonitorGroup;
        let panelAlloc = new Clutter.ActorBox({ x1: 0, y1: 0, x2: this.geom.w, y2: this.geom.h });
        let assignGroupSize = (group, update) => {
            group.size = 0;
            group.tlOffset = 0;
            group.brOffset = 0;

            group.elements.forEach(element => {
                if (!update) {
                    element.box[this.fixedCoord.c1] = panelAlloc[this.fixedCoord.c1];
                    element.box[this.fixedCoord.c2] = panelAlloc[this.fixedCoord.c2];
                    element.natSize = element.actor[this.sizeFunc](-1)[1];
                }

                if (!group.isCentered || Pos.checkIfCentered(element.position)) {
                    group.size += element.natSize;
                } else if (element.position == Pos.STACKED_TL) {
                    group.tlOffset += element.natSize;
                } else { // Pos.STACKED_BR
                    group.brOffset += element.natSize;
                }
            });

            if (group.isCentered) {
                group.size += Math.max(group.tlOffset, group.brOffset) * 2;
                group.tlOffset = Math.max(group.tlOffset - group.brOffset, 0);
            }
        };
        let allocateGroup = (group, tlLimit, brLimit) => {
            let startPosition = tlLimit;
            let currentPosition = 0;

            if (group.expandableIndex >= 0) {
                let availableSize = brLimit - tlLimit;
                let expandable = group.elements[group.expandableIndex];
                let i = 0;
                let l = this._elementGroups.length;
                let tlSize = 0;
                let brSize = 0;

                if (centeredMonitorGroup && (centeredMonitorGroup != group || expandable.position != Pos.CENTERED_MONITOR)) {
                    if (centeredMonitorGroup.index < group.index || (centeredMonitorGroup == group && expandable.position == Pos.STACKED_TL)) {
                        i = centeredMonitorGroup.index;
                    } else {
                        l = centeredMonitorGroup.index;
                    }
                }

                for (; i < l; ++i) {
                    let refGroup = this._elementGroups[i];

                    if (i < group.index && (!refGroup.fixed || refGroup[this.varCoord.c2] > tlLimit)) {
                        tlSize += refGroup.size;
                    } else if (i > group.index && (!refGroup.fixed || refGroup[this.varCoord.c1] < brLimit)) {
                        brSize += refGroup.size;
                    }
                }
                
                if (group.isCentered) {
                    availableSize -= Math.max(tlSize, brSize) * 2;
                } else {
                    availableSize -= tlSize + brSize;
                }
                
                if (availableSize < group.size) {
                    expandable.natSize -= (group.size - availableSize) * (group.isCentered && !Pos.checkIfCentered(expandable.position) ? .5 : 1);
                    assignGroupSize(group, true);
                }
            }
            
            if (group.isCentered) {
                startPosition = tlLimit + (brLimit - tlLimit - group.size) * .5;
            } else if (group.position == Pos.STACKED_BR) {
                startPosition = brLimit - group.size;
            }

            currentPosition = group.tlOffset + startPosition;

            group.elements.forEach(element => {
                element.box[this.varCoord.c1] = Math.round(currentPosition);
                element.box[this.varCoord.c2] = Math.round((currentPosition += element.natSize));

                element.actor.allocate(element.box);
            });

            group[this.varCoord.c1] = startPosition;
            group[this.varCoord.c2] = currentPosition;
            group.fixed = 1;
            ++fixed;
        };

        this.panel.allocate(panelAlloc);

        this._elementGroups.forEach(group => {
            group.fixed = 0;

            assignGroupSize(group);

            if (group.position == Pos.CENTERED_MONITOR) {
                centeredMonitorGroup = group;
            }
        });

        if (centeredMonitorGroup) {
            allocateGroup(centeredMonitorGroup, panelAlloc[this.varCoord.c1], panelAlloc[this.varCoord.c2]);
        }

        let iterations = 0; //failsafe
        while (fixed < this._elementGroups.length && ++iterations < 10) {
            for (let i = 0, l = this._elementGroups.length; i < l; ++i) {
                let group = this._elementGroups[i];

                if (group.fixed) {
                    continue;
                }

                let prevGroup = this._elementGroups[i - 1];
                let nextGroup = this._elementGroups[i + 1];
                let prevLimit = prevGroup && prevGroup.fixed ? prevGroup[this.varCoord.c2] : 
                                    centeredMonitorGroup && group.index > centeredMonitorGroup.index ? centeredMonitorGroup[this.varCoord.c2] : panelAlloc[this.varCoord.c1];
                let nextLimit = nextGroup && nextGroup.fixed ? nextGroup[this.varCoord.c1] : 
                                    centeredMonitorGroup && group.index < centeredMonitorGroup.index ? centeredMonitorGroup[this.varCoord.c1] : panelAlloc[this.varCoord.c2];

                if (group.position == Pos.STACKED_TL) {
                    allocateGroup(group, panelAlloc[this.varCoord.c1], nextLimit);
                } else if (group.position == Pos.STACKED_BR) {
                    allocateGroup(group, prevLimit, panelAlloc[this.varCoord.c2]);
                } else if ((!prevGroup || prevGroup.fixed) && (!nextGroup || nextGroup.fixed)) { // CENTERED
                    allocateGroup(group, prevLimit, nextLimit);
                }
            }
        }
    }

    _setPanelPosition() {
        let clipContainer = this.panelBox.get_parent();

        this.set_size(this.geom.w, this.geom.h);
        clipContainer.set_position(this.geom.x, this.geom.y);

        this._setVertical(this.panel, this.checkIfVertical());

        // styles for theming
        Object.keys(St.Side).forEach(p => {
            let cssName = 'dashtopanel' + p.charAt(0) + p.slice(1).toLowerCase();
            
            this.panel[(St.Side[p] == this.geom.position ? 'add' : 'remove') + '_style_class_name'](cssName);
        });

        this._setPanelClip(clipContainer);

        Main.layoutManager._updateHotCorners();
        Main.layoutManager._updatePanelBarrier(this);
    }

    _setPanelClip(clipContainer) {
        clipContainer = clipContainer || this.panelBox.get_parent();
        this._timeoutsHandler.add([T7, 0, () => Utils.setClip(clipContainer, clipContainer.x, clipContainer.y, this.panelBox.width, this.panelBox.height)]);
    }

    _onButtonPress(actor, event) {
        let type = event.type();
        let isPress = type == Clutter.EventType.BUTTON_PRESS;
        let button = isPress ? event.get_button() : -1;
        let [stageX, stageY] = event.get_coords();

        if (button == 3 && global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, stageX, stageY) == this.panel) {
            //right click on an empty part of the panel, temporarily borrow and display the showapps context menu
            Main.layoutManager.setDummyCursorGeometry(stageX, stageY, 0, 0);

            this.showAppsIconWrapper.createMenu();
            this.showAppsIconWrapper.popupMenu(Main.layoutManager.dummyCursor);

            return Clutter.EVENT_STOP;
        } else {
            const targetActor = global.stage.get_event_actor(event);

            if (Main.modalCount > 0 || targetActor != actor || 
                (!isPress && type != Clutter.EventType.TOUCH_BEGIN) ||
                (isPress && button != 1)) {
                return Clutter.EVENT_PROPAGATE;
            }
        }

        let params = this.checkIfVertical() ? [stageY, 'y', 'height'] : [stageX, 'x', 'width'];
        let dragWindow = this._getDraggableWindowForPosition.apply(this, params.concat(['maximized_' + this.getOrientation() + 'ly']));

        if (!dragWindow)
            return Clutter.EVENT_PROPAGATE;

        global.display.begin_grab_op(dragWindow,
                                     Meta.GrabOp.MOVING,
                                     false, /* pointer grab */
                                     true, /* frame action */
                                     button,
                                     event.get_state(),
                                     event.get_time(),
                                     stageX, stageY);

        return Clutter.EVENT_STOP;
    }

    _getDraggableWindowForPosition(stageCoord, coord, dimension, maximizedProp) {
        let workspace = Utils.getCurrentWorkspace();
        let allWindowsByStacking = global.display.sort_windows_by_stacking(
            workspace.list_windows()
        ).reverse();

        return Utils.find(allWindowsByStacking, metaWindow => {
            let rect = metaWindow.get_frame_rect();

            return metaWindow.get_monitor() == this.monitor.index &&
                   metaWindow.showing_on_its_workspace() &&
                   metaWindow.get_window_type() != Meta.WindowType.DESKTOP &&
                   metaWindow[maximizedProp] &&
                   stageCoord > rect[coord] && stageCoord < rect[coord] + rect[dimension];
        });
    }

    _onBoxActorAdded(box) {
        if (this.checkIfVertical()) {
            this._setVertical(box, true);
        }
    }

    _refreshVerticalAlloc() {
        this._setVertical(this._centerBox, true);
        this._setVertical(this._rightBox, true);
        this._formatVerticalClock();
    }

    _setVertical(actor, isVertical) {
        let _set = (actor, isVertical) => {
            if (!actor || actor instanceof Dash.DashItemContainer || actor instanceof TaskbarItemContainer.TaskbarItemContainer) {
                return;
            }

            if (actor instanceof St.BoxLayout) {
                actor.vertical = isVertical;
            } else if ((actor._delegate || actor) instanceof PanelMenu.ButtonBox && actor != this.statusArea.appMenu) {
                let child = actor.get_first_child();

                if (isVertical && !actor.visible && !actor._dtpVisibleId) {
                    this._unmappedButtons.push(actor);
                    actor._dtpVisibleId = actor.connect('notify::visible', () => {
                        this._disconnectVisibleId(actor);
                        this._refreshVerticalAlloc();
                    });
                    actor._dtpDestroyId = actor.connect('destroy', () => this._disconnectVisibleId(actor));
                }

                if (child) {
                    let [, natWidth] = actor.get_preferred_width(-1);

                    child.x_align = Clutter.ActorAlign[isVertical ? 'CENTER' : 'START'];
                    actor.set_width(isVertical ? this.dtpSize : -1);
                    isVertical = isVertical && (natWidth > this.dtpSize);
                    actor[(isVertical ? 'add' : 'remove') + '_style_class_name']('vertical');
                }
            }

            actor.get_children().forEach(c => _set(c, isVertical));
        };

        _set(actor, false);
        
        if (isVertical)
            _set(actor, isVertical);
    }

    _disconnectVisibleId(actor) {
        actor.disconnect(actor._dtpVisibleId);
        actor.disconnect(actor._dtpDestroyId);

        delete actor._dtpVisibleId;
        delete actor._dtpDestroyId;
        
        this._unmappedButtons.splice(this._unmappedButtons.indexOf(actor), 1);
    }

    _formatVerticalClock() {
        // https://github.com/GNOME/gnome-desktop/blob/master/libgnome-desktop/gnome-wall-clock.c#L310
        if (this.statusArea.dateMenu) {
            let datetime = this.statusArea.dateMenu._clock.clock;
            let datetimeParts = datetime.split(' ');
            let time = datetimeParts[1];
            let clockText = this.statusArea.dateMenu._clockDisplay.clutter_text;
            let setClockText = (text, useTimeSeparator) => {
                let stacks = text instanceof Array;
                let separator = `\n<span size="8192"> ${useTimeSeparator ? '‧‧' : '—' } </span>\n`;
        
                clockText.set_text((stacks ? text.join(separator) : text).trim());
                clockText.set_use_markup(stacks);
                clockText.get_allocation_box();
        
                return !clockText.get_layout().is_ellipsized();
            };

            if (clockText.ellipsize == Pango.EllipsizeMode.NONE) {
                //on gnome-shell 3.36.4, the clockdisplay isn't ellipsize anymore, so set it back 
                clockText.ellipsize = Pango.EllipsizeMode.END;
            }

            clockText.natural_width = this.dtpSize;

            if (!time) {
                datetimeParts = datetime.split(' ');
                time = datetimeParts.pop();
                datetimeParts = [datetimeParts.join(' '), time];
            }

            if (!setClockText(datetime) && 
                !setClockText(datetimeParts) && 
                !setClockText(time)) {
                let timeParts = time.split('∶');

                if (!this._clockFormat) {
                    this._clockFormat = DESKTOPSETTINGS.get_string('clock-format');
                }

                if (this._clockFormat == '12h') {
                    timeParts.push.apply(timeParts, timeParts.pop().split(' '));
                }

                setClockText(timeParts, true);
            }
        }
    }

    _setShowDesktopButton(add) {
        if (add) {
            if(this._showDesktopButton)
                return;

            this._showDesktopButton = new St.Bin({ style_class: 'showdesktop-button',
                            reactive: true,
                            can_focus: true,
                            // x_fill: true,
                            // y_fill: true,
                            track_hover: true });

            this._setShowDesktopButtonStyle();

            this._showDesktopButton.connect('button-press-event', () => this._onShowDesktopButtonPress());
            this._showDesktopButton.connect('enter-event', () => {
                this._showDesktopButton.add_style_class_name(this._getBackgroundBrightness() ?
                            'showdesktop-button-light-hovered' : 'showdesktop-button-dark-hovered');

                if (SETTINGS.get_boolean('show-showdesktop-hover')) {
                    this._timeoutsHandler.add([T4, SETTINGS.get_int('show-showdesktop-delay'), () => {
                        this._hiddenDesktopWorkspace = Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace();
                        this._toggleWorkspaceWindows(true, this._hiddenDesktopWorkspace);
                    }]);
                }
            });
            
            this._showDesktopButton.connect('leave-event', () => {
                this._showDesktopButton.remove_style_class_name(this._getBackgroundBrightness() ?
                            'showdesktop-button-light-hovered' : 'showdesktop-button-dark-hovered');

                if (SETTINGS.get_boolean('show-showdesktop-hover')) {
                    if (this._timeoutsHandler.getId(T4)) {
                        this._timeoutsHandler.remove(T4);
                    } else if (this._hiddenDesktopWorkspace) {
                        this._toggleWorkspaceWindows(false, this._hiddenDesktopWorkspace);
                    }
                }
            });

            this.panel.add_child(this._showDesktopButton);
        } else {
            if(!this._showDesktopButton)
                return;

            this.panel.remove_child(this._showDesktopButton);
            this._showDesktopButton.destroy();
            this._showDesktopButton = null;
        }
    }

    _setShowDesktopButtonStyle() {
        let rgb = this._getBackgroundBrightness() ? "rgba(55, 55, 55, .2)" : "rgba(200, 200, 200, .2)";

        let isLineCustom = SETTINGS.get_boolean('desktop-line-use-custom-color');
        rgb = isLineCustom ? SETTINGS.get_string('desktop-line-custom-color') : rgb;

        if (this._showDesktopButton) {
            let buttonSize = SETTINGS.get_int('showdesktop-button-width') + 'px;';
            let isVertical = this.checkIfVertical();

            let sytle = "border: 0 solid " + rgb + ";";
            sytle += isVertical ? 'border-top-width:1px;height:' + buttonSize : 'border-left-width:1px;width:' + buttonSize;

            this._showDesktopButton.set_style(sytle);
            this._showDesktopButton[(isVertical ? 'x' : 'y') + '_expand'] = true;
        }
    }

    // _getBackgroundBrightness: return true if panel has a bright background color
    _getBackgroundBrightness() {
        return Utils.checkIfColorIsBright(this.dynamicTransparency.backgroundColorRgb);
    }

    _toggleWorkspaceWindows(hide, workspace) {
        let time = SETTINGS.get_int('show-showdesktop-time') * .001;

        workspace.list_windows().forEach(w => {
            if (!w.minimized && !w.customJS_ding) {
                let tweenOpts = {
                    opacity: hide ? 0 : 255,
                    time: time,
                    transition: 'easeOutQuad'
                };
                
                Utils.animateWindowOpacity(w.get_compositor_private(), tweenOpts);
            }
        });
    }

    _onShowDesktopButtonPress() {
        let label = 'trackerFocusApp';

        this._signalsHandler.removeWithLabel(label);
        this._timeoutsHandler.remove(T5);

        if(this._restoreWindowList && this._restoreWindowList.length) {
            this._timeoutsHandler.remove(T4);

            let current_workspace = Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace();
            let windows = current_workspace.list_windows();
            this._restoreWindowList.forEach(function(w) {
                if(windows.indexOf(w) > -1)
                    Main.activateWindow(w);
            });
            this._restoreWindowList = null;
        } else {
            let current_workspace = Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace();
            let windows = current_workspace.list_windows().filter(function (w) {
                return w.showing_on_its_workspace() && !w.skip_taskbar;
            });
            windows = global.display.sort_windows_by_stacking(windows);

            windows.forEach(function(w) {
                w.minimize();
            });
            
            this._restoreWindowList = windows;

            this._timeoutsHandler.add([T5, 20, () => this._signalsHandler.addWithLabel(
                label, 
                [
                    tracker, 
                    'notify::focus-app', 
                    () => this._restoreWindowList = null
                ]
            )]);
        }

        Main.overview.hide();
    }

    _onPanelMouseScroll(actor, event) {
        let scrollAction = SETTINGS.get_string('scroll-panel-action');
        let direction = Utils.getMouseScrollDirection(event);

        const targetActor = global.stage.get_event_actor(event);

        if (!this._checkIfIgnoredScrollSource(targetActor) && !this._timeoutsHandler.getId(T6)) {
            if (direction && scrollAction === 'SWITCH_WORKSPACE') {
                let args = [global.display];

                //adjust for horizontal workspaces
                if (Utils.DisplayWrapper.getWorkspaceManager().layout_rows === 1) {
                    direction = direction == 'up' ? 'left' : 'right';
                }

                //gnome-shell < 3.30 needs an additional "screen" param
                global.screen ? args.push(global.screen) : 0;

                let showWsPopup = SETTINGS.get_boolean('scroll-panel-show-ws-popup');
                showWsPopup ? 0 : Main.wm._workspaceSwitcherPopup = { display: () => {} };
                Main.wm._showWorkspaceSwitcher.apply(Main.wm, args.concat([0, { get_name: () => 'switch---' + direction }]));
                showWsPopup ? 0 : Main.wm._workspaceSwitcherPopup = null;
            } else if (direction && scrollAction === 'CYCLE_WINDOWS') {
                let windows = this.taskbar.getAppInfos().reduce((ws, appInfo) => ws.concat(appInfo.windows), []);
                
                Utils.activateSiblingWindow(windows, direction);
            } else if (scrollAction === 'CHANGE_VOLUME' && !event.is_pointer_emulated()) {
                let proto = Volume.OutputIndicator.prototype;
                let func = proto._handleScrollEvent || proto.vfunc_scroll_event || proto._onScrollEvent;
                let indicator = Main.panel.statusArea[Utils.getSystemMenuInfo().name]._volumeOutput;

                if (indicator.quickSettingsItems)
                    // new quick settings menu in gnome-shell > 42
                    func(indicator.quickSettingsItems[0], event);
                else
                    func.call(indicator, 0, event);
            } else {
                return;
            }

            const scrollDelay = SETTINGS.get_int('scroll-panel-delay');

            if (scrollDelay) {
                this._timeoutsHandler.add([T6, scrollDelay, () => {}]);
            }
        }
    }

    _checkIfIgnoredScrollSource(source) {
        let ignoredConstr = ['WorkspaceIndicator'];

        return source.get_parent()._dtpIgnoreScroll || ignoredConstr.indexOf(source.constructor.name) >= 0;
    }

    _initProgressManager() {
        const progressVisible = SETTINGS.get_boolean('progress-show-bar');
        const countVisible = SETTINGS.get_boolean('progress-show-count');
        const pm = this.progressManager;

        if(!pm && (progressVisible || countVisible))
            this.progressManager = new Progress.ProgressManager();
        else if (pm)
            Object.keys(pm._entriesByDBusName).forEach((k) => pm._entriesByDBusName[k].setCountVisible(countVisible));
    }
});

export const SecondaryPanel = GObject.registerClass({
}, class SecondaryPanel extends St.Widget {

    _init(params) {
        super._init(params);
    }

    vfunc_allocate(box) {
        this.set_allocation(box);
    }
});
