/* exported WorkspaceIndicator */
const { Clutter, Gio, GObject, Meta, St } = imports.gi;

const DND = imports.ui.dnd;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Tweener = imports.ui.tweener;

const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;

const TOOLTIP_OFFSET = 6;
const TOOLTIP_ANIMATION_TIME = 150;

let WindowPreview = GObject.registerClass({
    GTypeName: 'WindowListWindowPreview'
}, class WindowPreview extends St.Button {
    _init(window) {
        super._init({
            style_class: 'window-list-window-preview'
        });

        this._delegate = this;
        DND.makeDraggable(this, { restoreOnSuccess: true });

        this._window = window;

        this.connect('destroy', this._onDestroy.bind(this));

        this._sizeChangedId = this._window.connect('size-changed',
            this._relayout.bind(this));
        this._positionChangedId = this._window.connect('position-changed',
            this._relayout.bind(this));
        this._minimizedChangedId = this._window.connect('notify::minimized',
            this._relayout.bind(this));
        this._monitorEnteredId = global.display.connect('window-entered-monitor',
            this._relayout.bind(this));
        this._monitorLeftId = global.display.connect('window-left-monitor',
            this._relayout.bind(this));

        // Do initial layout when we get a parent
        let id = this.connect('parent-set', () => {
            this.disconnect(id);
            if (!this.get_parent())
                return;
            this._laterId = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
                this._laterId = 0;
                this._relayout();
                return false;
            });
        });

        this._focusChangedId = global.display.connect('notify::focus-window',
            this._onFocusChanged.bind(this));
        this._onFocusChanged();
    }

    // needed for DND
    get realWindow() {
        return this._window.get_compositor_private();
    }

    _onDestroy() {
        this._window.disconnect(this._sizeChangedId);
        this._window.disconnect(this._positionChangedId);
        this._window.disconnect(this._minimizedChangedId);
        global.display.disconnect(this._monitorEnteredId);
        global.display.disconnect(this._monitorLeftId);
        global.display.disconnect(this._focusChangedId);
        if (this._laterId)
            Meta.later_remove(this._laterId);
    }

    _onFocusChanged() {
        if (global.display.focus_window == this._window)
            this.add_style_class_name('active');
        else
            this.remove_style_class_name('active');
    }

    _relayout() {
        let monitor = Main.layoutManager.findIndexForActor(this);
        this.visible = monitor == this._window.get_monitor() &&
            this._window.showing_on_its_workspace();

        if (!this.visible)
            return;

        let workArea = Main.layoutManager.getWorkAreaForMonitor(monitor);
        let hscale = this.get_parent().allocation.get_width() / workArea.width;
        let vscale = this.get_parent().allocation.get_height() / workArea.height;

        let frameRect = this._window.get_frame_rect();
        this.set_size(
            Math.round(Math.min(frameRect.width, workArea.width) * hscale),
            Math.round(Math.min(frameRect.height, workArea.height) * vscale));
        this.set_position(
            Math.round(frameRect.x * hscale),
            Math.round(frameRect.y * vscale));
    }
});

let WorkspaceThumbnail = GObject.registerClass({
    GTypeName: 'WindowListWorkspaceThumbnail'
}, class WorkspaceThumbnail extends St.Button {
    _init(index) {
        super._init({
            style_class: 'workspace',
            child: new Clutter.Actor({
                layout_manager: new Clutter.BinLayout(),
                clip_to_allocation: true
            }),
            x_fill: true,
            y_fill: true
        });

        this._tooltip = new St.Label({
            style_class: 'dash-label',
            visible: false,
        });
        Main.uiGroup.add_child(this._tooltip);

        this.connect('destroy', this._onDestroy.bind(this));
        this.connect('notify::hover', this._syncTooltip.bind(this));

        this._index = index;
        this._delegate = this; // needed for DND

        this._windowPreviews = new Map();

        let workspaceManager = global.workspace_manager;
        this._workspace = workspaceManager.get_workspace_by_index(index);

        this._windowAddedId = this._workspace.connect('window-added',
            (ws, window) => {
                this._addWindow(window);
            });
        this._windowRemovedId = this._workspace.connect('window-removed',
            (ws, window) => {
                this._removeWindow(window);
            });
        this._restackedId = global.display.connect('restacked',
            this._onRestacked.bind(this));

        this._workspace.list_windows().forEach(w => this._addWindow(w));
        this._onRestacked();
    }

    acceptDrop(source) {
        if (!source.realWindow)
            return false;

        let window = source.realWindow.get_meta_window();
        this._moveWindow(window);
        return true;
    }

    handleDragOver(source) {
        if (source.realWindow)
            return DND.DragMotionResult.MOVE_DROP;
        else
            return DND.DragMotionResult.CONTINUE;
    }

    _addWindow(window) {
        if (this._windowPreviews.has(window))
            return;

        let preview = new WindowPreview(window);
        preview.connect('clicked', (a, btn) => this.emit('clicked', btn));
        this._windowPreviews.set(window, preview);
        this.child.add_child(preview);
    }

    _removeWindow(window) {
        let preview = this._windowPreviews.get(window);
        if (!preview)
            return;

        this._windowPreviews.delete(window);
        preview.destroy();
    }

    _onRestacked() {
        let lastPreview = null;
        let windows = global.get_window_actors().map(a => a.meta_window);
        for (let i = 0; i < windows.length; i++) {
            let preview = this._windowPreviews.get(windows[i]);
            if (!preview)
                continue;

            this.child.set_child_above_sibling(preview, lastPreview);
            lastPreview = preview;
        }
    }

    _moveWindow(window) {
        let monitorIndex = Main.layoutManager.findIndexForActor(this);
        if (monitorIndex != window.get_monitor())
            window.move_to_monitor(monitorIndex);
        window.change_workspace_by_index(this._index, false);
    }

    // eslint-disable-next-line camelcase
    on_clicked() {
        let ws = global.workspace_manager.get_workspace_by_index(this._index);
        if (ws)
            ws.activate(global.get_current_time());
    }

    _syncTooltip() {
        if (this.hover) {
            this._tooltip.text = Meta.prefs_get_workspace_name(this._index);
            this._tooltip.opacity = 0;
            this._tooltip.show();

            const [stageX, stageY] = this.get_transformed_position();
            const thumbWidth = this.allocation.get_width();
            const tipWidth = this._tooltip.width;
            const tipHeight = this._tooltip.height;
            const xOffset = Math.floor((thumbWidth - tipWidth) / 2);
            const monitor = Main.layoutManager.findMonitorForActor(this);
            const x = Math.min(
                Math.max(stageX + xOffset, monitor.x),
                monitor.x + monitor.width - tipWidth);
            const y = stageY - tipHeight - TOOLTIP_OFFSET;
            this._tooltip.set_position(x, y);
        }

        Tweener.addTween(this._tooltip, {
            opacity: this.hover ? 255 : 0,
            time: TOOLTIP_ANIMATION_TIME * 1000,
            transition: 'easeOutQuad',
            onComplete: () => (this._tooltip.visible = this.hover),
        });
    }

    _onDestroy() {
        this._tooltip.destroy();

        this._workspace.disconnect(this._windowAddedId);
        this._workspace.disconnect(this._windowRemovedId);
        global.display.disconnect(this._restackedId);
    }
});

var WorkspaceIndicator = GObject.registerClass({
    GTypeName: 'WindowListWorkspaceIndicator'
}, class WorkspaceIndicator extends PanelMenu.Button {
    _init() {
        super._init(0.0, _('Workspace Indicator'), true);
        this.setMenu(new PopupMenu.PopupMenu(this, 0.0, St.Side.BOTTOM));
        this.add_style_class_name('window-list-workspace-indicator');
        this.menu.actor.remove_style_class_name('panel-menu');

        let container = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true
        });
        this.add_actor(container);

        let workspaceManager = global.workspace_manager;

        this._currentWorkspace = workspaceManager.get_active_workspace_index();
        this._statusLabel = new St.Label({ text: this._getStatusText() });

        this._statusBin = new St.Bin({
            style_class: 'status-label-bin',
            x_expand: true,
            y_expand: true,
            child: this._statusLabel
        });
        container.add_actor(this._statusBin);

        this._thumbnailsBox = new St.BoxLayout({
            style_class: 'workspaces-box',
            y_expand: true,
            reactive: true
        });
        this._thumbnailsBox.connect('scroll-event',
            this._onScrollEvent.bind(this));
        container.add_actor(this._thumbnailsBox);

        this._workspacesItems = [];

        this._workspaceManagerSignals = [
            workspaceManager.connect('notify::n-workspaces',
                this._nWorkspacesChanged.bind(this)),
            workspaceManager.connect_after('workspace-switched',
                this._onWorkspaceSwitched.bind(this)),
            workspaceManager.connect('notify::layout-rows',
                this._onWorkspaceOrientationChanged.bind(this))
        ];

        this.connect('scroll-event', this._onScrollEvent.bind(this));
        this._updateMenu();
        this._updateThumbnails();
        this._onWorkspaceOrientationChanged();

        this._settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.wm.preferences' });
        this._settingsChangedId = this._settings.connect(
            'changed::workspace-names', this._updateMenuLabels.bind(this));
    }

    _onDestroy() {
        for (let i = 0; i < this._workspaceManagerSignals.length; i++)
            global.workspace_manager.disconnect(this._workspaceManagerSignals[i]);

        if (this._settingsChangedId) {
            this._settings.disconnect(this._settingsChangedId);
            this._settingsChangedId = 0;
        }

        super._onDestroy();
    }

    _onWorkspaceOrientationChanged() {
        let vertical = global.workspace_manager.layout_rows == -1;
        this.reactive = vertical;

        this._statusBin.visible = vertical;
        this._thumbnailsBox.visible = !vertical;
    }

    _onWorkspaceSwitched() {
        let workspaceManager = global.workspace_manager;
        this._currentWorkspace = workspaceManager.get_active_workspace_index();

        this._updateMenuOrnament();
        this._updateActiveThumbnail();

        this._statusLabel.set_text(this._getStatusText());
    }

    _nWorkspacesChanged() {
        this._updateMenu();
        this._updateThumbnails();
    }

    _updateMenuOrnament() {
        for (let i = 0; i < this._workspacesItems.length; i++) {
            this._workspacesItems[i].setOrnament(i == this._currentWorkspace
                ? PopupMenu.Ornament.DOT
                : PopupMenu.Ornament.NONE);
        }
    }

    _updateActiveThumbnail() {
        let thumbs = this._thumbnailsBox.get_children();
        for (let i = 0; i < thumbs.length; i++) {
            if (i == this._currentWorkspace)
                thumbs[i].add_style_class_name('active');
            else
                thumbs[i].remove_style_class_name('active');
        }
    }

    _getStatusText() {
        let workspaceManager = global.workspace_manager;
        let current = workspaceManager.get_active_workspace_index();
        let total = workspaceManager.n_workspaces;

        return '%d / %d'.format(current + 1, total);
    }

    _updateMenuLabels() {
        for (let i = 0; i < this._workspacesItems.length; i++) {
            let item = this._workspacesItems[i];
            let name = Meta.prefs_get_workspace_name(i);
            item.label.text = name;
        }
    }

    _updateMenu() {
        let workspaceManager = global.workspace_manager;

        this.menu.removeAll();
        this._workspacesItems = [];
        this._currentWorkspace = workspaceManager.get_active_workspace_index();

        for (let i = 0; i < workspaceManager.n_workspaces; i++) {
            let name = Meta.prefs_get_workspace_name(i);
            let item = new PopupMenu.PopupMenuItem(name);
            item.workspaceId = i;

            item.connect('activate', (item, _event) => {
                this._activate(item.workspaceId);
            });

            if (i == this._currentWorkspace)
                item.setOrnament(PopupMenu.Ornament.DOT);

            this.menu.addMenuItem(item);
            this._workspacesItems[i] = item;
        }

        this._statusLabel.set_text(this._getStatusText());
    }

    _updateThumbnails() {
        let workspaceManager = global.workspace_manager;

        this._thumbnailsBox.destroy_all_children();

        for (let i = 0; i < workspaceManager.n_workspaces; i++) {
            let thumb = new WorkspaceThumbnail(i);
            this._thumbnailsBox.add_actor(thumb);
        }
        this._updateActiveThumbnail();
    }

    _activate(index) {
        let workspaceManager = global.workspace_manager;

        if (index >= 0 && index < workspaceManager.n_workspaces) {
            let metaWorkspace = workspaceManager.get_workspace_by_index(index);
            metaWorkspace.activate(global.get_current_time());
        }
    }

    _onScrollEvent(actor, event) {
        let direction = event.get_scroll_direction();
        let diff = 0;
        if (direction == Clutter.ScrollDirection.DOWN) {
            diff = 1;
        } else if (direction == Clutter.ScrollDirection.UP) {
            diff = -1;
        } else {
            return;
        }

        let newIndex = this._currentWorkspace + diff;
        this._activate(newIndex);
    }

    _allocate(actor, box, flags) {
        if (actor.get_n_children() > 0)
            actor.get_first_child().allocate(box, flags);
    }
});

