import App from 'resource:///com/github/Aylur/ags/app.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Service from 'resource:///com/github/Aylur/ags/service.js';
import { subprocess } from 'resource:///com/github/Aylur/ags/utils.js';
import GUdev from 'gi://GUdev';

const UDEV_POINTERS = [
    'ID_INPUT_MOUSE',
    'ID_INPUT_POINTINGSTICK',
    'ID_INPUT_TOUCHPAD',
    'ID_INPUT_TOUCHSCREEN',
    'ID_INPUT_TABLET',
];
const ON_RELEASE_TRIGGERS = [
    'released',
    'TOUCH_UP',
    'HOLD_END',
];
const ON_CLICK_TRIGGERS = [
    'pressed',
    'TOUCH_DOWN',
];


class Pointers extends Service {
    static {
        Service.register(this, {
            'proc-started': ['boolean'],
            'proc-destroyed': ['boolean'],
            'device-fetched': ['boolean'],
            'new-line': ['string'],
            'released': ['string'],
            'clicked': ['string'],
        });
    }

    #process;
    #lastLine = '';
    #pointers = [];
    #udevClient = GUdev.Client.new(['input']);

    get process() {
        return this.#process;
    }

    get lastLine() {
        return this.#lastLine;
    }

    get pointers() {
        return this.#pointers;
    }

    constructor() {
        super();
        this.#initUdevConnection();
        this.#initAppConnection();
    }

    // FIXME: logitech mouse screws everything up on disconnect
    #initUdevConnection() {
        this.#getDevices();
        this.#udevClient.connect('uevent', (_, action) => {
            if (action === 'add' || action === 'remove') {
                this.getDevices();
                if (this.#process) {
                    this.killProc();
                    this.startProc();
                }
            }
        });
    }

    #getDevices() {
        this.#pointers = [];
        this.#udevClient.query_by_subsystem('input').forEach((dev) => {
            const isPointer = UDEV_POINTERS.some((p) => dev.has_property(p));

            if (isPointer) {
                const hasEventFile = dev.has_property('DEVNAME') &&
                                     dev.get_property('DEVNAME')
                                         .includes('event');

                if (hasEventFile) {
                    this.#pointers.push(dev.get_property('DEVNAME'));
                }
            }
        });

        this.emit('device-fetched', true);
    }

    startProc() {
        if (this.#process) {
            return;
        }

        const args = [];

        this.#pointers.forEach((dev) => {
            args.push('--device');
            args.push(dev);
        });

        this.#process = subprocess(
            ['libinput', 'debug-events', ...args],
            (output) => {
                if (output.includes('cancelled')) {
                    return;
                }

                if (ON_RELEASE_TRIGGERS.some((p) => output.includes(p))) {
                    this.#lastLine = output;
                    Pointers.detectClickedOutside('released');
                    this.emit('released', output);
                    this.emit('new-line', output);
                }

                if (ON_CLICK_TRIGGERS.some((p) => output.includes(p))) {
                    this.#lastLine = output;
                    Pointers.detectClickedOutside('clicked');
                    this.emit('clicked', output);
                    this.emit('new-line', output);
                }
            },
            (err) => logError(err),
        );
        this.emit('proc-started', true);
    }

    killProc() {
        if (this.#process) {
            this.#process.force_exit();
            this.#process = null;
            this.emit('proc-destroyed', true);
        }
    }

    #initAppConnection() {
        App.connect('window-toggled', () => {
            const anyVisibleAndClosable = Array.from(App.windows).some((w) => {
                const closable = w[1].closeOnUnfocus &&
                                !(w[1].closeOnUnfocus === 'none' ||
                                    w[1].closeOnUnfocus === 'stay');

                return w[1].visible && closable;
            });

            if (anyVisibleAndClosable) {
                this.startProc();
            }

            else {
                this.killProc();
            }
        });
    }

    static detectClickedOutside(clickStage) {
        const toClose = Array.from(App.windows).some((w) => {
            const closable = (w[1].closeOnUnfocus &&
                              w[1].closeOnUnfocus === clickStage);

            return w[1].visible && closable;
        });

        if (!toClose) {
            return;
        }

        Hyprland.sendMessage('j/layers').then((layers) => {
            layers = JSON.parse(layers);

            Hyprland.sendMessage('j/cursorpos').then((pos) => {
                pos = JSON.parse(pos);

                Object.values(layers).forEach((key) => {
                    const bar = key.levels['3']
                        .find((n) => n.namespace === 'bar');

                    const widgets = key.levels['3'].filter((n) => {
                        const window = App.getWindow(n.namespace);

                        return window?.closeOnUnfocus &&
                               window?.closeOnUnfocus === clickStage;
                    });

                    if (pos.x > bar?.x && pos.x < bar?.x + bar?.w &&
                        pos.y > bar?.y && pos.y < bar?.y + bar?.h) {

                        // Don't handle clicks when on bar
                        // TODO: make this configurable
                    }
                    else {
                        widgets.forEach((w) => {
                            if (!(pos.x > w.x && pos.x < w.x + w.w &&
                                  pos.y > w.y && pos.y < w.y + w.h)) {
                                App.closeWindow(w.namespace);
                            }
                        });
                    }
                });
            }).catch(print);
        }).catch(print);
    }
}

export default new Pointers();