/**
 * V-Shell (Vertical Workspaces)
 * WinTmb
 *
 * @author     GdH <G-dH@github.com>
 * @copyright  2021-2023
 * @license    GPL-3.0
 */

'use strict';

const Clutter = imports.gi.Clutter;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Meta = imports.gi.Meta;
const St = imports.gi.St;

const AltTab = imports.ui.altTab;
const DND = imports.ui.dnd;
const Main = imports.ui.main;

let Me;
let opt;

const SCROLL_ICON_OPACITY = 240;
const DRAG_OPACITY = 200;
const CLOSE_BTN_OPACITY = 240;


var WinTmbModule = class {
    constructor(me) {
        Me = me;
        opt = Me.opt;

        this._firstActivation = true;
        this.moduleEnabled = false;
    }

    cleanGlobals() {
        Me = null;
        opt = null;
    }

    update(reset) {
        this._removeTimeouts();

        this.moduleEnabled = opt.get('windowThumbnailModule');

        reset = reset || !this.moduleEnabled;

        // don't touch the original code if module disabled
        if (reset && !this._firstActivation) {
            this._disableModule();
        } else if (!reset) {
            this._firstActivation = false;
            this._activateModule();
        }
        if (reset && this._firstActivation)
            console.debug('  WinTmb - Keeping untouched');
    }

    _activateModule() {
        this._timeouts = {};
        if (!this._windowThumbnails)
            this._windowThumbnails = [];

        Main.overview.connectObject('hidden', () => this.showThumbnails(), this);
        console.debug('  WinTmb - Activated');
    }

    _disableModule() {
        Main.overview.disconnectObject(this);
        this._disconnectStateAdjustment();
        this.removeAllThumbnails();
        console.debug('  WinTmb - Disabled');
    }

    _removeTimeouts() {
        if (this._timeouts) {
            Object.values(this._timeouts).forEach(t => {
                if (t)
                    GLib.source_remove(t);
            });
            this._timeouts = null;
        }
    }

    createThumbnail(metaWin) {
        const thumbnail = new WindowThumbnail(metaWin, {
            'height': Math.floor(opt.WINDOW_THUMBNAIL_SCALE * global.display.get_monitor_geometry(global.display.get_current_monitor()).height),
            'thumbnailsOnScreen': this._windowThumbnails.length,
        });

        this._windowThumbnails.push(thumbnail);
        thumbnail.connect('removed', tmb => {
            this._windowThumbnails.splice(this._windowThumbnails.indexOf(tmb), 1);
            tmb.destroy();
            if (!this._windowThumbnails.length)
                this._disconnectStateAdjustment();
        });

        if (!this._stateAdjustmentConId) {
            this._stateAdjustmentConId = Main.overview._overview.controls._stateAdjustment.connectObject('notify::value', () => {
                if (!this._thumbnailsHidden && (!opt.OVERVIEW_MODE2 || opt.WORKSPACE_MODE))
                    this.hideThumbnails();
            }, this);
        }
    }

    hideThumbnails() {
        this._windowThumbnails.forEach(tmb => {
            tmb.ease({
                opacity: 0,
                duration: 200,
                mode: Clutter.AnimationMode.LINEAR,
                onComplete: () => tmb.hide(),
            });
        });
        this._thumbnailsHidden = true;
    }

    showThumbnails() {
        this._windowThumbnails.forEach(tmb => {
            tmb.show();
            tmb.ease({
                opacity: 255,
                duration: 100,
                mode: Clutter.AnimationMode.LINEAR,
            });
        });
        this._thumbnailsHidden = false;
    }

    removeAllThumbnails() {
        this._windowThumbnails.forEach(tmb => tmb.remove());
        this._windowThumbnails = [];
    }

    _disconnectStateAdjustment() {
        Main.overview._overview.controls._stateAdjustment.disconnectObject(this);
    }
};

const WindowThumbnail = GObject.registerClass({
    Signals: { 'removed': {} },
}, class WindowThumbnail extends St.Widget {
    _init(metaWin, args) {
        this._hoverShowsPreview = false;
        this._customOpacity = 255;
        this._initTmbHeight = args.height;
        this._minimumHeight = Math.floor(5 / 100 * global.display.get_monitor_geometry(global.display.get_current_monitor()).height);
        this._scrollTimeout = 100;
        this._positionOffset = args.thumbnailsOnScreen;
        this._reverseTmbWheelFunc = false;
        this._click_count = 1;
        this._prevBtnPressTime = 0;
        this.w = metaWin;
        super._init({
            layout_manager: new Clutter.BinLayout(),
            visible: true,
            reactive: true,
            can_focus: true,
            track_hover: true,
        });
        this.connect('button-release-event', this._onBtnReleased.bind(this));
        this.connect('scroll-event', this._onScrollEvent.bind(this));
        // this.connect('motion-event', this._onMouseMove.bind(this)); // may be useful in the future..

        this._delegate = this;
        this._draggable = DND.makeDraggable(this, { dragActorOpacity: DRAG_OPACITY });
        this._draggable.connect('drag-end', this._end_drag.bind(this));
        this._draggable.connect('drag-cancelled', this._end_drag.bind(this));
        this._draggable._animateDragEnd = eventTime => {
            this._draggable._animationInProgress = true;
            this._draggable._onAnimationComplete(this._draggable._dragActor, eventTime);
            this.opacity = this._customOpacity;
        };

        this.clone = new Clutter.Clone({ reactive: true });
        Main.layoutManager.addChrome(this);

        this.window = this.w.get_compositor_private();

        this.clone.set_source(this.window);

        this.add_child(this.clone);
        this._addCloseButton();
        this._addScrollModeIcon();

        this.connect('enter-event', () => {
            global.display.set_cursor(Meta.Cursor.POINTING_HAND);
            this._closeButton.opacity = CLOSE_BTN_OPACITY;
            this._scrollModeBin.opacity = SCROLL_ICON_OPACITY;
            if (this._hoverShowsPreview && !Main.overview._shown) {
                this._closeButton.opacity = 50;
                this._showWindowPreview(false, true);
            }
        });

        this.connect('leave-event', () => {
            global.display.set_cursor(Meta.Cursor.DEFAULT);
            this._closeButton.opacity = 0;
            this._scrollModeBin.opacity = 0;
            if (this._winPreview)
                this._destroyWindowPreview();
        });

        this._setSize(true);
        this.set_position(...this._getInitialPosition());
        this.show();
        this.window_id = this.w.get_id();
        this.tmbRedrawDirection = true;

        // remove thumbnail content and hide thumbnail if its window is destroyed
        this.windowConnect = this.window.connect('destroy', () => {
            if (this)
                this.remove();
        });
    }

    _getInitialPosition() {
        const offset = 20;
        let monitor = Main.layoutManager.monitors[global.display.get_current_monitor()];
        let x = Math.min(monitor.x + monitor.width  - (this.window.width  * this.scale) - offset);
        let y = Math.min(monitor.y + monitor.height - (this.window.height * this.scale) - offset - ((this._positionOffset * this._initTmbHeight) % (monitor.height - this._initTmbHeight)));
        return [x, y];
    }

    _setSize(resetScale = false) {
        if (resetScale)
            this.scale = Math.min(1.0, this._initTmbHeight / this.window.height);

        const width = this.window.width * this.scale;
        const height = this.window.height * this.scale;
        this.set_size(width, height);
        if (this.icon) {
            this.icon.scale_x = this.scale;
            this.icon.scale_y = this.scale;
        }

        // when the scale of this. actor change, this.clone resize accordingly,
        // but the reactive area of the actor doesn't change until the actor is redrawn
        // this updates the actor's input region area:
        Main.layoutManager._queueUpdateRegions();
    }

    /* _onMouseMove(actor, event) {
        let [pos_x, pos_y] = event.get_coords();
        let state = event.get_state();
        if (this._ctrlPressed(state)) {
        }
    }*/

    _onBtnReleased(actor, event) {
        // Clutter.Event.click_count property in no longer available, since GS42
        if ((event.get_time() - this._prevBtnPressTime) < Clutter.Settings.get_default().double_click_time)
            this._click_count += 1;
        else
            this._click_count = 1;

        this._prevBtnPressTime = event.get_time();

        if (this._click_count === 2 && event.get_button() === Clutter.BUTTON_PRIMARY)
            this.w.activate(global.get_current_time());


        const button = event.get_button();
        const state = event.get_state();
        switch (button) {
        case Clutter.BUTTON_PRIMARY:
            if (this._ctrlPressed(state)) {
                this._setSize();
            } else {
                this._reverseTmbWheelFunc = !this._reverseTmbWheelFunc;
                this._scrollModeBin.set_child(this._reverseTmbWheelFunc ? this._scrollModeSourceIcon : this._scrollModeResizeIcon);
            }
            return Clutter.EVENT_STOP;
        case Clutter.BUTTON_SECONDARY:
            if (this._ctrlPressed(state)) {
                this.remove();
            } else {
                this._hoverShowsPreview = !this._hoverShowsPreview;
                this._showWindowPreview();
            }
            return Clutter.EVENT_STOP;
        case Clutter.BUTTON_MIDDLE:
            if (this._ctrlPressed(state))
                this.w.delete(global.get_current_time());
            return Clutter.EVENT_STOP;
        default:
            return Clutter.EVENT_PROPAGATE;
        }
    }

    _onScrollEvent(actor, event) {
        let direction = Me.Util.getScrollDirection(event);

        if (this._actionTimeoutActive())
            return Clutter.EVENT_PROPAGATE;
        let state = event.get_state();
        switch (direction) {
        case Clutter.ScrollDirection.UP:
            if (this._shiftPressed(state)) {
                this.opacity = Math.min(255, this.opacity + 24);
                this._customOpacity = this.opacity;
            } else if (this._reverseTmbWheelFunc !== this._ctrlPressed(state)) {
                this._switchSourceWin(-1);
            } else if (this._reverseTmbWheelFunc === this._ctrlPressed(state)) {
                this.scale = Math.max(0.05, this.scale - 0.025);
            }
            break;
        case Clutter.ScrollDirection.DOWN:
            if (this._shiftPressed(state)) {
                this.opacity = Math.max(48, this.opacity - 24);
                this._customOpacity = this.opacity;
            } else if (this._reverseTmbWheelFunc !== this._ctrlPressed(state)) {
                this._switchSourceWin(+1);
            } else if (this._reverseTmbWheelFunc === this._ctrlPressed(state)) {
                this.scale = Math.min(1, this.scale + 0.025);
            }
            break;
        default:
            return Clutter.EVENT_PROPAGATE;
        }
        this._setSize();
        return Clutter.EVENT_STOP;
    }

    remove() {
        if (this.clone) {
            this.window.disconnect(this.windowConnect);
            this.clone.set_source(null);
        }
        if (this._winPreview)
            this._destroyWindowPreview();

        this.emit('removed');
    }

    _end_drag() {
        this.set_position(this._draggable._dragOffsetX + this._draggable._dragX, this._draggable._dragOffsetY + this._draggable._dragY);
        this._setSize();
    }

    _ctrlPressed(state) {
        return (state & Clutter.ModifierType.CONTROL_MASK) !== 0;
    }

    _shiftPressed(state) {
        return (state & Clutter.ModifierType.SHIFT_MASK) !== 0;
    }

    _switchSourceWin(direction) {
        let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null);
        windows = windows.filter(w => !(w.skip_taskbar || w.minimized));
        let idx = -1;
        for (let i = 0; i < windows.length; i++) {
            if (windows[i] === this.w) {
                idx = i + direction;
                break;
            }
        }
        idx = idx >= windows.length ? 0 : idx;
        idx = idx < 0 ? windows.length - 1 : idx;
        let w = windows[idx];
        let win = w.get_compositor_private();
        this.clone.set_source(win);
        this.window.disconnect(this.windowConnect);
        // the new thumbnail should be the same height as the previous one
        this.scale = (this.scale * this.window.height) / win.height;
        this.window = win;
        this.windowConnect = this.window.connect('destroy', () => {
            if (this)
                this.remove();
        });
        this.w = w;

        if (this._winPreview)
            this._showWindowPreview(true);
    }

    _actionTimeoutActive() {
        const timeout = this._reverseTmbWheelFunc ? this._scrollTimeout : this._scrollTimeout / 4;
        if (!this._lastActionTime || Date.now() - this._lastActionTime > timeout) {
            this._lastActionTime = Date.now();
            return false;
        }
        return true;
    }

    /* _setIcon() {
        let tracker = Shell.WindowTracker.get_default();
        let app = tracker.get_window_app(this.w);
        let icon = app
            ? app.create_icon_texture(this.height)
            : new St.Icon({ icon_name: 'icon-missing', icon_size: this.height });
        icon.x_expand = icon.y_expand = true;
        if (this.icon)
            this.icon.destroy();
        this.icon = icon;
    }*/

    _addCloseButton() {
        const closeButton = new St.Button({
            opacity: 0,
            style_class: 'window-close',
            child: new St.Icon({ icon_name: 'preview-close-symbolic' }),
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.START,
            x_expand: true,
            y_expand: true,
        });

        closeButton.set_style(`
            margin: 3px;
            background-color: rgba(200, 0, 0, 0.9);
        `);

        closeButton.connect('clicked', () => {
            this.remove();
            return Clutter.EVENT_STOP;
        });

        this._closeButton = closeButton;
        this.add_child(this._closeButton);
    }

    _addScrollModeIcon() {
        this._scrollModeBin = new St.Bin({
            x_expand: true,
            y_expand: true,
        });
        this._scrollModeResizeIcon = new St.Icon({
            icon_name: 'view-fullscreen-symbolic',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.END,
            x_expand: true,
            y_expand: true,
            opacity: SCROLL_ICON_OPACITY,
            style_class: 'icon-dropshadow',
            scale_x: 0.5,
            scale_y: 0.5,
        });
        this._scrollModeResizeIcon.set_style(`
            margin: 13px;
            color: rgb(255, 255, 255);
            box-shadow: 0 0 40px 40px rgba(0,0,0,0.7);
        `);
        this._scrollModeSourceIcon = new St.Icon({
            icon_name: 'media-skip-forward-symbolic',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.END,
            x_expand: true,
            y_expand: true,
            opacity: SCROLL_ICON_OPACITY,
            style_class: 'icon-dropshadow',
            scale_x: 0.5,
            scale_y: 0.5,
        });
        this._scrollModeSourceIcon.set_style(`
            margin: 13px;
            color: rgb(255, 255, 255);
            box-shadow: 0 0 40px 40px rgba(0,0,0,0.7);
        `);
        this._scrollModeBin.set_child(this._scrollModeResizeIcon);
        this.add_child(this._scrollModeBin);
        this._scrollModeBin.opacity = 0;
    }

    _showWindowPreview(update = false, dontDestroy = false) {
        if (this._winPreview && !dontDestroy) {
            this._destroyWindowPreview();
            this._previewCreationTime = 0;
            this._closeButton.opacity = CLOSE_BTN_OPACITY;
            if (!update)
                return;
        }

        if (!this._winPreview) {
            this._winPreview = new AltTab.CyclerHighlight();
            global.window_group.add_actor(this._winPreview);
            [this._winPreview._xPointer, this._winPreview._yPointer] = global.get_pointer();
        }

        if (!update) {
            this._winPreview.opacity = 0;
            this._winPreview.ease({
                opacity: 255,
                duration: 70,
                mode: Clutter.AnimationMode.LINEAR,
                /* onComplete: () => {
                    this._closeButton.opacity = 50;
                },*/
            });

            this.ease({
                opacity: Math.min(50, this._customOpacity),
                duration: 70,
                mode: Clutter.AnimationMode.LINEAR,
                onComplete: () => {
                },
            });
        } else {
            this._winPreview.opacity = 255;
        }
        this._winPreview.window = this.w;
        this._winPreview._window = this.w;
        global.window_group.set_child_above_sibling(this._winPreview, null);
    }

    _destroyWindowPreview() {
        if (this._winPreview) {
            this._winPreview.ease({
                opacity: 0,
                duration: 100,
                mode: Clutter.AnimationMode.LINEAR,
                onComplete: () => {
                    this._winPreview.destroy();
                    this._winPreview = null;
                    this.opacity = this._customOpacity;
                },
            });
        }
    }
});
