/*
 * Compiz-windows-effect for GNOME Shell
 *
 * Copyright (C) 2020
 *     Mauro Pepe <https://github.com/hermes83/compiz-windows-effect>
 *
 * This file is part of the gnome-shell extension Compiz-windows-effect.
 *
 * gnome-shell extension Compiz-windows-effect is free software: you can
 * redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * gnome-shell extension Compiz-windows-effect is distributed in the hope that it
 * will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with gnome-shell extension Compiz-windows-effect.  If not, see
 * <http://www.gnu.org/licenses/>.
 */
'use strict';

import Meta from 'gi://Meta';

import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';

import { SettingsData } from './settings_data.js';
import { WobblyEffect } from './src/effects/wobbly.js';
import { ResizeEffect } from './src/effects/resize.js';

const EFFECT_NAME = 'wobbly-compiz-effect';

export default class CompizWindowsEffectExtension extends Extension {

    enable() {
        this.settingsData = new SettingsData(this.getSettings());
        if (this.settingsData.PRESET.get() === 'P' &&
            this.settingsData.FRICTION.get() === 3.5 && 
            this.settingsData.SPRING_K.get() === 3.8 && 
            this.settingsData.SPEEDUP_FACTOR.get() === 12.0 && 
            this.settingsData.MASS.get() === 70.0) 
        {
            this.settingsData.PRESET.set('R');
        }
        
        let currentVersion = this.metadata.version;
        let lastVersion = this.settingsData.LAST_VERSION.get();
        if (lastVersion < currentVersion) {
            console.log(`[CompizWindowsEffectExtension] version updated: ${lastVersion} -> ${currentVersion}`);
        }
        this.settingsData.LAST_VERSION.set(currentVersion);

        this.allowedResizeOp = [Meta.GrabOp.RESIZING_W, Meta.GrabOp.RESIZING_E, Meta.GrabOp.RESIZING_S, Meta.GrabOp.RESIZING_N, Meta.GrabOp.RESIZING_NW, Meta.GrabOp.RESIZING_NE, Meta.GrabOp.RESIZING_SE, Meta.GrabOp.RESIZING_SW];

        this.grabOpBeginId = null;
        this.grabOpEndId = null;
        this.resizedActor = null;
        this.startResizeOpId = null;
        this.endResizeOpId = null;
        this.destroyId = null;

        this.grabOpBeginId = global.display.connect('grab-op-begin', (display, window, op) => {
            if (Meta.GrabOp.MOVING != op && 
                (!Meta.GrabOp.MOVING_UNCONSTRAINED || Meta.GrabOp.MOVING_UNCONSTRAINED != op) &&
                (!this.settingsData.RESIZE_EFFECT.get() || this.allowedResizeOp.indexOf(op) === -1)) {
                return;
            }
            
            let actor = window ? window.get_compositor_private() : null;
            if (!actor) {
                return;
            }

            this.destroyActorEffect(actor);
            actor.remove_effect_by_name(EFFECT_NAME);

            if (Meta.GrabOp.MOVING === op || Meta.GrabOp.MOVING_UNCONSTRAINED === op) {
                actor.add_effect_with_name(EFFECT_NAME, new WobblyEffect({settingsData: this.settingsData, op: 'move'}));
            } else {
                actor.add_effect_with_name(EFFECT_NAME, new ResizeEffect({settingsData: this.settingsData, op: op}));
            }
        });
    
        this.grabOpEndId = global.display.connect('grab-op-end', (display, window, op) => {  
            let actor = window ? window.get_compositor_private() : null;
            if (!actor) {
                return;
            }
            
            let effect = actor.get_effect(EFFECT_NAME);
            if (effect) {
                effect.on_end_event(actor);
            }
        });

        this.startResizeOpId = global.window_manager.connect('size-change', (wm, actor, op, oldFrameRect, oldBufferRect) => {
            if (!actor) {
                return;
            }

            this.resizedActor = actor;
            this.resizedActor.sourceRect = actor.meta_window.get_frame_rect();

            if (!op || Meta.SizeChange.UNMAXIMIZE != op) {
                return;
            }

            let effect = actor.get_effect(EFFECT_NAME);
            if (!effect || effect.operationType != 'move') {
                this.destroyActorEffect(actor);
                actor.add_effect_with_name(EFFECT_NAME, new WobblyEffect({settingsData: this.settingsData, op: 'unmaximized'}));
            }
        });

        this.endResizeOpId = global.window_manager.connect('size-changed', (wm, actor) => {
            if (!actor || !this.resizedActor || actor != this.resizedActor || !this.resizedActor.sourceRect) {
                this.resizedActor = null;
                return;
            }

            const sourceRect = this.resizedActor.sourceRect;
            const targetRect = actor.meta_window.get_frame_rect();

            this.resizedActor = null;

            const metaWindowMaximized = actor.metaWindow.get_maximize_flags();
            if (metaWindowMaximized) {
                this.destroyActorEffect(actor);

                if (!this.settingsData.MAXIMIZE_EFFECT.get()) {
                    return;
                }

                const monitor = Main.layoutManager.monitors[actor.meta_window.get_monitor()];
                
                if (metaWindowMaximized === Meta.MaximizeFlags.BOTH || 
                        (
                            metaWindowMaximized === Meta.MaximizeFlags.VERTICAL && 
                            (
                                (sourceRect.y != targetRect.y) || 
                                (sourceRect.y + sourceRect.height != targetRect.y + targetRect.height) || 
                                (sourceRect.x === monitor.x && targetRect.x != monitor.x) || 
                                (sourceRect.x != monitor.x && targetRect.x === monitor.x) ||
                                (sourceRect.x + sourceRect.width === monitor.x + monitor.width && targetRect.x + targetRect.width != monitor.x + monitor.width) || 
                                (sourceRect.x + sourceRect.width != monitor.x + monitor.width && targetRect.x + targetRect.width === monitor.x + monitor.width)
                            )
                        )
                    ) 
                {        
                    actor.add_effect_with_name(EFFECT_NAME, new WobblyEffect({settingsData: this.settingsData, op: 'maximized'}));
                }
            } else {
                let effect = actor.get_effect(EFFECT_NAME);
                if (effect && 'move' === effect.operationType) {
                    this.destroyActorEffect(actor);
                    actor.add_effect_with_name(EFFECT_NAME, new WobblyEffect({settingsData: this.settingsData, op: 'move'}));
                } 
            }
        });

        this.destroyId = global.window_manager.connect("destroy", (wm, actor) => {
            this.destroyActorEffect(actor);
        });
    }

    disable() {
        if (this.settingsData) {
            this.settingsData = null;
        }
        if (this.resizedActor) {
            this.resizedActor = null;
        }
        if (this.allowedResizeOp) {
            this.allowedResizeOp = null;
        }
        if (this.grabOpBeginId) {
            global.display.disconnect(this.grabOpBeginId);
            this.grabOpBeginId = null;
        }
        if (this.grabOpEndId) {
            global.display.disconnect(this.grabOpEndId);
            this.grabOpEndId = null;
        }
        if (this.endResizeOpId) {
            global.window_manager.disconnect(this.endResizeOpId);
            this.endResizeOpId = null;
        }
        if (this.startResizeOpId) {
            global.window_manager.disconnect(this.startResizeOpId);
            this.startResizeOpId = null;
        }
        if (this.destroyId) {
            global.window_manager.disconnect(this.destroyId);
            this.destroyId = null;
        }
        
        global.get_window_actors().forEach((actor) => {
            this.destroyActorEffect(actor);
        });
    }

    destroyActorEffect(actor) {
        if (!actor) {
            return;
        }

        let effect = actor.get_effect(EFFECT_NAME);
        if (effect) {
            effect.destroy();
        }
    }

}