/**
 * Vertical Workspaces
 * windowPreview.js
 * 
 * @author     GdH <G-dH@github.com>
 * @copyright  2022 - 2023
 * @license    GPL-3.0
 *
 */

'use strict';

const { Clutter, GLib, Graphene, Meta, Shell, St } = imports.gi;

const Main = imports.ui.main;
const WindowPreview = imports.ui.windowPreview;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();

const _Util = Me.imports.util;
let _overrides;

const WINDOW_SCALE_TIME = imports.ui.windowPreview.WINDOW_SCALE_TIME;
const WINDOW_ACTIVE_SIZE_INC = imports.ui.windowPreview.WINDOW_ACTIVE_SIZE_INC;
const WINDOW_OVERLAY_FADE_TIME = imports.ui.windowPreview.WINDOW_OVERLAY_FADE_TIME;
const SEARCH_WINDOWS_PREFIX = Me.imports.windowSearchProvider.prefix;

const ControlsState = imports.ui.overviewControls.ControlsState;

var opt = null;

function update(reset = false) {
    if (_overrides) {
        _overrides.removeAll();
    }

    if (reset) {
        _overrides = null;
        opt = null;
        return;
    }

    opt = Me.imports.settings.opt;
    _overrides = new _Util.Overrides();

    _overrides.addOverride('WindowPreview', WindowPreview.WindowPreview.prototype, WindowPreviewCommon);
    // move titles into window previews
    _overrides.addInjection('WindowPreview', WindowPreview.WindowPreview.prototype, WindowPreviewInjections);
}

//----- WindowPreview ------------------------------------------------------------------
var WindowPreviewInjections = {
    _init: function() {
        const ICON_OVERLAP = 0.7;

        if (opt.WIN_PREVIEW_ICON_SIZE < 64) {
            this.remove_child(this._icon);
            this._icon.destroy();
            const tracker = Shell.WindowTracker.get_default();
            const app = tracker.get_window_app(this.metaWindow);
            this._icon = app.create_icon_texture(opt.WIN_PREVIEW_ICON_SIZE);
            this._icon.add_style_class_name('icon-dropshadow');
            this._icon.set({
                reactive: true,
                pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
            });
            this._icon.add_constraint(new Clutter.BindConstraint({
                source: this.windowContainer,
                coordinate: Clutter.BindCoordinate.POSITION,
            }));
            this._icon.add_constraint(new Clutter.AlignConstraint({
                source: this.windowContainer,
                align_axis: Clutter.AlignAxis.X_AXIS,
                factor: 0.5,
            }));
            this._icon.add_constraint(new Clutter.AlignConstraint({
                source: this.windowContainer,
                align_axis: Clutter.AlignAxis.Y_AXIS,
                pivot_point: new Graphene.Point({ x: -1, y: ICON_OVERLAP }),
                factor: 1,
            }));
            this.add_child(this._icon);
            if (opt.WIN_PREVIEW_ICON_SIZE < 22) {
                // disable app icon
                this._icon.hide();
            }
            this._iconSize = opt.WIN_PREVIEW_ICON_SIZE;
        }

        const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
        const iconOverlap = opt.WIN_PREVIEW_ICON_SIZE * ICON_OVERLAP;
        // we cannot get proper title height before it gets to the stage, so 35 is estimated height + spacing
        this._title.get_constraints()[1].offset = scaleFactor * (- iconOverlap - 35);
        this.set_child_above_sibling(this._title, null);
        // if window is created while the overview is shown, icon and title should be visible immediately
        if (Main.overview._overview._controls._stateAdjustment.value < 1) {
            this._icon.scale_x = 0;
            this._icon.scale_y = 0;
            this._title.opacity = 0;
        }

        if (opt.ALWAYS_SHOW_WIN_TITLES) {
            this._title.show();
            this._stateConId = this._workspace._background._stateAdjustment.connect('notify::value', (adj) => {
                this._title.opacity = Math.floor(adj.value) * 255;
            });

        }

        if (opt.OVERVIEW_MODE === 1) {
            // spread windows on hover
            this._wsStateConId = this.connect('enter-event', () => {
                // don't spread windows if user don't use pointer device at this moment
                if (global.get_pointer()[0] === opt.showingPointerX)
                    return;

                const adjustment = this._workspace._background._stateAdjustment;
                if (!adjustment.value && !Main.overview._animationInProgress) {
                    opt.WORKSPACE_MODE = 1;
                    if (adjustment.value === 0) {
                        adjustment.value = 0;
                        adjustment.ease(1, {
                            duration: 200,
                            mode: Clutter.AnimationMode.EASE_OUT_QUAD
                        });
                    }
                }
            });
        }
        if (opt.OVERVIEW_MODE) {
            // show window icon and title on ws windows spread
            this._stateAdjustmentSigId = this._workspace.stateAdjustment.connect('notify::value', this._updateIconScale.bind(this));
        }

        // replace click action with custom one
        const action = this.get_actions()[0];
        this.remove_action(action);

        const clickAction = new Clutter.ClickAction();
        clickAction.connect('clicked', (action) => {
            const button = action.get_button();
            if (button === Clutter.BUTTON_PRIMARY) {
                this._activate();
            } else if (button === Clutter.BUTTON_SECONDARY) {
                // this action cancels long-press event and the 'long-press-cancel' event is used by the Shell to actually initiate DnD
                // so the dnd initiation needs to be removed
                if (this._longPressLater) {
                    Meta.later_remove(this._longPressLater);
                    delete this._longPressLater;
                }
                const tracker = Shell.WindowTracker.get_default();
                const appName = tracker.get_window_app(this.metaWindow).get_name();
                _Util.activateSearchProvider(`${SEARCH_WINDOWS_PREFIX} ${appName}`);
                return Clutter.EVENT_STOP;
            }
        });

        clickAction.connect('long-press', this._onLongPress.bind(this));
        this.add_action(clickAction);

        this.connect('destroy', () => this._workspace.stateAdjustment.disconnect(this._stateConId));
    }
}

// WindowPreview
var WindowPreviewCommon = {
    _updateIconScale: function() {
        let { currentState, initialState, finalState } =
            this._overviewAdjustment.getStateTransitionParams();

        // Current state - 0 - HIDDEN, 1 - WINDOW_PICKER, 2 - APP_GRID
        const primaryMonitor = this.metaWindow.get_monitor() === global.display.get_primary_monitor();

        const visible =
            (initialState > ControlsState.HIDDEN || finalState > ControlsState.HIDDEN)
            && !(finalState === ControlsState.APP_GRID && primaryMonitor);

        let scale = visible
            ? (currentState >= 1 ? 1 : currentState % 1) : 0;
        if (!primaryMonitor &&
            ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
            (initialState === ControlsState.APP_GRID && finalState === ControlsState.WINDOW_PICKER))
            ) {
            scale = 1;
        /*} else if (primaryMonitor && ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
            initialState === ControlsState.APP_GRID && finalState === ControlsState.HIDDEN)) {*/
        } else if (primaryMonitor && currentState > ControlsState.WINDOW_PICKER) {
            scale = 0;
        }

        // in static workspace mode show icon and title on ws windows spread
        if (opt.OVERVIEW_MODE) {
            const windowsSpread = this._workspace.stateAdjustment.value;
            if (currentState === 1) {
                scale = windowsSpread;
            } else if (finalState === 1 || (finalState === 0 && !windowsSpread)) {
                return;
            }
        }

        this._icon.set({
            scale_x: scale,
            scale_y: scale,
        });

        // if titles are in 'always show' mode, we need to add transition between visible/invisible state
        // but the transition is quite expensive,
        // showing the titles at the end of the transition is good enough and workspace preview transition is much smoother
        this._title.set({
            opacity: Math.floor(scale) * 255,
        });
    },

    showOverlay: function(animate) {
        if (!this._overlayEnabled)
            return;

        if (this._overlayShown)
            return;

        this._overlayShown = true;
        //this._restack();

        // If we're supposed to animate and an animation in our direction
        // is already happening, let that one continue
        const ongoingTransition = this._title.get_transition('opacity');
        if (animate &&
            ongoingTransition &&
            ongoingTransition.get_interval().peek_final_value() === 255)
            return;

        const toShow = this._windowCanClose()
            ? [this._closeButton]
            : [];

        if (!opt.ALWAYS_SHOW_WIN_TITLES) {
            toShow.push(this._title);
        }

        toShow.forEach(a => {
            a.opacity = 0;
            a.show();
            a.ease({
                opacity: 255,
                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        });

        const [width, height] = this.window_container.get_size();
        const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
        const activeExtraSize = WINDOW_ACTIVE_SIZE_INC * 2 * scaleFactor;
        const origSize = Math.max(width, height);
        const scale = (origSize + activeExtraSize) / origSize;

        this.window_container.ease({
            scale_x: scale,
            scale_y: scale,
            duration: animate ? WINDOW_SCALE_TIME : 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        this.emit('show-chrome');
    },

    hideOverlay: function(animate) {
        if (!this._overlayShown)
            return;
        this._overlayShown = false;
        //this._restack();

        // If we're supposed to animate and an animation in our direction
        // is already happening, let that one continue
        const ongoingTransition = this._title.get_transition('opacity');
        if (animate &&
            ongoingTransition &&
            ongoingTransition.get_interval().peek_final_value() === 0)
            return;

        const toHide = [this._closeButton];

        if (!opt.ALWAYS_SHOW_WIN_TITLES) {
            toHide.push(this._title);
        }
        toHide.forEach(a => {
            a.opacity = 255;
            a.ease({
                opacity: 0,
                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => a.hide(),
            });
        });

        if (this.window_container) {
            this.window_container.ease({
                scale_x: 1,
                scale_y: 1,
                duration: animate ? WINDOW_SCALE_TIME : 0,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }
    },

    _onDestroy() {
        // fix for upstream bug - hideOverlay is called after windowPreview is destroyed, from the leave event callback
        // but it still throws:
        //  clutter_actor_get_preferred_width: assertion 'CLUTTER_IS_ACTOR (self)' failed
        //  clutter_actor_get_preferred_height: assertion 'CLUTTER_IS_ACTOR (self)' failed
        this.hideOverlay(false);

        this.metaWindow._delegate = null;
        this._delegate = null;

        if (this._longPressLater) {
            Meta.later_remove(this._longPressLater);
            delete this._longPressLater;
        }

        if (this._idleHideOverlayId > 0) {
            GLib.source_remove(this._idleHideOverlayId);
            this._idleHideOverlayId = 0;
        }

        if (this.inDrag) {
            this.emit('drag-end');
            this.inDrag = false;
        }

        if (this._stateAdjustmentSigId) {
            this._workspace.stateAdjustment.disconnect(this._stateAdjustmentSigId);
        }
    }
}
