/* placesManager.js
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

/* exported PlacesBookmarksManager PlacesRecentManager PlacesTrashManager PlacesVolumesManager */

const Gettext = imports.gettext;
const Signals = imports.signals;

const { Gio, GLib, Gtk, Shell } = imports.gi;

const ExtensionUtils = imports.misc.extensionUtils;

const Main = imports.ui.main;
const { ShellMountOperation } = imports.ui.shellMountOperation;

const Me = ExtensionUtils.getCurrentExtension();

const _ = Gettext.domain(Me.metadata['gettext-domain']).gettext;

class PlaceInfo {
    constructor(...params) {
        this._init(...params);
    }

    _init(file, name, icon) {
        this.file = file;
        this.name = name || this._getFileName();
        this.icon = icon ? new Gio.ThemedIcon({ name: icon }) : this._getIcon();
    }

    _getFileName() {
        try {
            let info = this.file.query_info('standard::display-name', 0, null);
            return info.get_display_name();
        } catch (e) {
            if (e instanceof Gio.IOErrorEnum) return this.file.get_basename();
            throw e;
        }
    }

    _getIcon() {
        try {
            let info = this.file.query_info('standard::icon', 0, null);
            return info.get_icon();
        } catch (e) {
            if (e instanceof Gio.IOErrorEnum) {
                if (!this.file.is_native()) return new Gio.ThemedIcon({ name: 'folder-remote' });
                return new Gio.ThemedIcon({ name: 'folder' });
            } else {
                throw e;
            }
        }
    }

    _notifyError(msg, e) {
        Main.notifyError(msg.format(this.name), e.message);
    }

    destroy() {}

    launch(timestamp) {
        let launchContext = global.create_app_launch_context(timestamp, -1);
        try {
            Gio.AppInfo.launch_default_for_uri(this.file.get_uri(), launchContext);
        } catch (e) {
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) {
                let source = { get_icon: () => this.icon };
                let op = new ShellMountOperation(source);
                let mountArgs = [
                    Gio.MountMountFlags.NONE,
                    op.mountOp,
                    null, // Gio.Cancellable
                ];
                this.file.mount_enclosing_volume(...mountArgs, (file, result) => {
                    try {
                        file.mount_enclosing_volume_finish(result);
                        Gio.AppInfo.launch_default_for_uri(file.get_uri(), launchContext);
                    } catch (err) {
                        if (!err.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED)) this._notifyError(_('Failed to mount volume for »%s«'), err);
                    } finally {
                        op.close();
                    }
                });
            } else {
                this._notifyError(_('Failed to launch »%s«'), e);
            }
        }
    }

    get can_eject() {
        return false;
    }

    get can_empty() {
        return false;
    }

    get can_remove() {
        return false;
    }

    get can_restore() {
        return false;
    }
}

class PlaceMountInfo extends PlaceInfo {
    _init(mount) {
        this._mount = mount;
        super._init(mount.get_root(), mount.get_name());
    }

    _getIcon() {
        return this._mount.get_icon();
    }

    eject() {
        let op = new ShellMountOperation(this._mount);
        let unmountArgs = [
            Gio.MountUnmountFlags.NONE,
            op.mountOp,
            null, // Gio.Cancellable
        ];
        if (this._mount.can_eject()) {
            this._mount.eject_with_operation(...unmountArgs, (mount, result) => {
                try {
                    mount.eject_with_operation_finish(result);
                } catch (e) {
                    this._notifyError(_('Ejecting drive »%s« failed:'), e);
                } finally {
                    op.close();
                }
            });
        } else {
            this._mount.unmount_with_operation(...unmountArgs, (mount, result) => {
                try {
                    mount.unmount_with_operation_finish(result);
                } catch (e) {
                    this._notifyError(_('Failed to unmount »%s«'), e);
                } finally {
                    op.close();
                }
            });
        }
    }

    get can_eject() {
        if (!this._mount.can_eject() && !this._mount.can_unmount()) return false;
        if (this._mount.is_shadowed()) return false;
        let volume = this._mount.get_volume();
        if (!volume) return true;
        return volume.get_identifier('class') !== 'network';
    }
}

class PlaceRecentInfo extends PlaceInfo {
    _init(recentManager, file) {
        this._recentManager = recentManager;
        super._init(file);
    }

    empty() {
        this._recentManager.purge_items();
    }

    get can_empty() {
        return true;
    }
}

class PlaceRecentItemInfo extends PlaceInfo {
    _init(recentManager, file) {
        this._recentManager = recentManager;
        super._init(file);
    }

    _getFileName() {
        return this.file.get_display_name();
    }

    _getIcon() {
        return Gio.content_type_get_icon(this.file.get_mime_type());
    }

    launch(timestamp) {
        let launchContext = global.create_app_launch_context(timestamp, -1);
        try {
            Gio.AppInfo.launch_default_for_uri(this.file.get_uri(), launchContext);
        } catch (e) {
            this._notifyError(_('Failed to open »%s«'), e);
        }
    }

    remove() {
        this._recentManager.remove_item(this.file.get_uri());
    }

    get can_remove() {
        return true;
    }
}

class PlaceTrashInfo extends PlaceInfo {
    _init(file) {
        super._init(file);
    }

    empty() {
        let child = null;
        let children = this.file.enumerate_children('*', 0, null);
        while ((child = children.next_file(null)) !== null) {
            this.file.get_child(child.get_name()).delete(null);
        }
        children.close(null);
    }

    get can_empty() {
        return true;
    }
}

class PlaceTrashItemInfo extends PlaceInfo {
    _init(trash, file) {
        this._trash = trash;
        super._init(file);
    }

    _getFileName() {
        return this.file.get_display_name();
    }

    _getIcon() {
        return this.file.get_icon();
    }

    launch(timestamp) {
        let launchContext = global.create_app_launch_context(timestamp, -1);
        try {
            let file = this._trash.get_child(this.file.get_name());
            Gio.AppInfo.launch_default_for_uri(file.get_uri(), launchContext);
        } catch (e) {
            this._notifyError(_('Failed to open »%s«'), e);
        }
    }

    remove() {
        this._trash.get_child(this.file.get_name()).delete(null);
    }

    restore() {
        let trash_orig_path = this.file.get_attribute_byte_string(Gio.FILE_ATTRIBUTE_TRASH_ORIG_PATH);
        let restoreFile = Gio.File.new_for_path(trash_orig_path);
        let restorePath = Gio.File.new_for_path(trash_orig_path.slice(0, trash_orig_path.lastIndexOf('/') + 1));
        try {
            if (!restorePath.query_exists(null)) restorePath.make_directory_with_parents(null);
            this._trash.get_child(this.file.get_name()).move(restoreFile, Gio.FileCopyFlags.NONE, null, null);
        } catch (e) {
            this._notifyError(_('Failed to restore »%s«'), e);
        }
    }

    get can_remove() {
        return true;
    }

    get can_restore() {
        return true;
    }

    get deletion_date() {
        return new Date(this.file.get_attribute_as_string(Gio.FILE_ATTRIBUTE_TRASH_DELETION_DATE));
    }
}

class PlacesManager {
    constructor(...params) {
        this._init(...params);
    }

    _init() {
        this.size = 0;
        this._places = {};
        /*
        * Show devices, code more or less ported from nautilus-places-sidebar.c
        */
        this._volumeMonitor = Gio.VolumeMonitor.get();
        this._volumeMonitorChangedId = [
            this._volumeMonitor.connect('drive-changed', this._reload.bind(this)),
            this._volumeMonitor.connect('drive-connected', this._reload.bind(this)),
            this._volumeMonitor.connect('drive-disconnected', this._reload.bind(this)),
            this._volumeMonitor.connect('mount-added', this._reload.bind(this)),
            this._volumeMonitor.connect('mount-changed', this._reload.bind(this)),
            this._volumeMonitor.connect('mount-removed', this._reload.bind(this)),
            this._volumeMonitor.connect('volume-added', this._reload.bind(this)),
            this._volumeMonitor.connect('volume-changed', this._reload.bind(this)),
            this._volumeMonitor.connect('volume-removed', this._reload.bind(this)),
        ];
        this._reload();
    }

    _reload() {
        this.emit('updated');
    }

    _sort(kind) {
        this._places[kind].sort((a, b) => GLib.utf8_collate(a.name, b.name));
    }

    destroy() {
        this._volumeMonitorChangedId.forEach(id => {
            this._volumeMonitor.disconnect(id);
        });
    }

    get(kind) {
        return this._places[kind];
    }
}
Signals.addSignalMethods(PlacesManager.prototype);

var PlacesBookmarksManager = class extends PlacesManager {
    _init() {
        this._bookmarksPath = GLib.build_filenamev([ GLib.get_user_config_dir(), 'gtk-3.0', 'bookmarks' ]);
        this._bookmarksFile = Gio.File.new_for_path(this._bookmarksPath);
        this._bookmarksMonitor = this._bookmarksFile.monitor_file(Gio.FileMonitorFlags.NONE, null);
        this._bookmarksTimeoutId = 0;
        this._bookmarksMonitor.connect('changed', () => {
            if (this._bookmarksTimeoutId > 0) return;
            /* Defensive event compression */
            this._bookmarksTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
                this._bookmarksTimeoutId = 0;
                this._reload();
                return GLib.SOURCE_REMOVE;
            });
        });
        super._init();
    }

    _addBookmark(kind, path, name) {
        let file = Gio.File.new_for_path(path);
        this._places[kind].push(new PlaceInfo(file, name));
        return file.get_basename();
    }

    _reload() {
        let bookmarksLabel = [];
        this._places['custom'] = [];
        this._places['default'] = [];
        this._places['special'] = [];
        /* Add standard bookmarks */
        bookmarksLabel.push(this._addBookmark('special', GLib.get_home_dir(), _('Home')));
        for (let i = 0; i < GLib.UserDirectory.N_DIRECTORIES; i++) {
            let specialPath = GLib.get_user_special_dir(i);
            if (!specialPath) continue;
            if (i === GLib.UserDirectory.DIRECTORY_DESKTOP) {
                bookmarksLabel.push(this._addBookmark('special', specialPath));
            } else {
                bookmarksLabel.push(this._addBookmark('default', specialPath));
            }
        }
        /* add all bookmarks that are not part of the standard bookmarks */
        if (GLib.file_test(this._bookmarksPath, GLib.FileTest.EXISTS)) {
            let bookmarksContent = Shell.get_file_contents_utf8_sync(this._bookmarksPath);
            let bookmarks = bookmarksContent.split('\n');
            for (let i = 0; i < bookmarks.length; i++) {
                let bookmarkLine = bookmarks[i];
                let components = bookmarkLine.split(' ');
                let bookmark = components[0];
                if (!bookmark) continue;
                let file = Gio.File.new_for_uri(bookmark);
                if (file.is_native() && !file.query_exists(null)) continue;
                if (!bookmarksLabel.includes(file.get_basename())) {
                    let label = components.length > 1 ? components.slice(1).join(' ') : null;
                    this._places['custom'].push(new PlaceInfo(file, label));
                }
            }
            this._sort('custom');
        }
        this._sort('default');
        super._reload();
    }

    destroy() {
        if (this._bookmarksMonitor) this._bookmarksMonitor.cancel();
        if (this._bookmarksTimeoutId) GLib.source_remove(this._bookmarksTimeoutId);
        super.destroy();
    }
};

var PlacesRecentManager = class extends PlacesManager {
    _init() {
        this._recentManager = Gtk.RecentManager.get_default();
        this._recentManagerChangedId = this._recentManager.connect('changed', this._reload.bind(this));
        super._init();
    }

    _reload() {
        this.size = 0;
        this._places['recent'] = [];
        this._places['recent-item'] = [];
        this._places['recent'].push(new PlaceRecentInfo(this._recentManager, Gio.File.new_for_uri('recent:///')));
        let items = this._recentManager.get_items();
        items.forEach(file => {
            if (GLib.file_test(file.get_uri_display(), GLib.FileTest.IS_REGULAR)) {
                this._places['recent-item'].push(new PlaceRecentItemInfo(this._recentManager, file));
                this.size++;
            }
        });
        this._sort('recent-item');
        super._reload();
    }

    destroy() {
        this._recentManager.disconnect(this._recentManagerChangedId);
        super.destroy();
    }
};

var PlacesTrashManager = class extends PlacesManager {
    _init() {
        this._trash = Gio.File.new_for_uri('trash:///');
        this._trashMonitor = this._trash.monitor_directory(Gio.FileMonitorFlags.NONE, null);
        this._trashTimeoutId = 0;
        this._trashMonitorChangedId = this._trashMonitor.connect('changed', () => {
            if (this._trashTimeoutId > 0) return;
            /* Defensive event compression */
            this._trashTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
                this._trashTimeoutId = 0;
                this._reload();
                return GLib.SOURCE_REMOVE;
            });
        });
        super._init();
    }

    _reload() {
        this.size = 0;
        this._places['trash'] = [];
        this._places['trash-item'] = [];
        this._places['trash'].push(new PlaceTrashInfo(this._trash));
        let children = this._trash.enumerate_children('*', 0, null);
        let file = null;
        while ((file = children.next_file(null)) !== null) {
            this._places['trash-item'].push(new PlaceTrashItemInfo(this._trash, file));
            this.size++;
        }
        children.close(null);
        this._sort('trash-item');
        super._reload();
    }

    _sort(kind) {
        this._places[kind].sort((a, b) => {
            return b.deletion_date - a.deletion_date;
        });
    }

    destroy() {
        if (this._trashTimeoutId) GLib.source_remove(this._trashTimeoutId);
        this._trashMonitor.disconnect(this._trashMonitorChangedId);
        super.destroy();
    }
};

var PlacesVolumesManager = class extends PlacesManager {
    _init() {
        super._init();
    }

    _addMount(kind, mount) {
        let info;
        try {
            info = new PlaceMountInfo(mount);
        } catch (e) {
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) return;
            throw e;
        }
        this._places[kind].push(info);
    }

    _reload() {
        this._places['device'] = [];
        this._places['network'] = [];
        this._places['special'] = [];
        /* Add standard places */
        this._places['special'].push(new PlaceInfo(Gio.File.new_for_uri('file:///'), _('Browse file system'), 'drive-harddisk'));
        this._places['special'].push(new PlaceInfo(Gio.File.new_for_uri('network:///'), _('Browse network'), 'network-workgroup'));
        /* first go through all connected drives */
        let drives = this._volumeMonitor.get_connected_drives();
        drives.forEach(drive => {
            let volumes = drive.get_volumes();
            volumes.forEach(volume => {
                let mount = volume.get_mount();
                if (mount) {
                    let identifier = volume.get_identifier('class');
                    let category = identifier && identifier.includes('network') ? 'network' : 'device';
                    this._addMount(category, mount);
                }
            });
        });
        /* add all volumes that is not associated with a drive */
        let volumes = this._volumeMonitor.get_volumes();
        volumes.forEach(volume => {
            if (!volume.get_drive()) {
                let mount = volume.get_mount();
                if (mount) {
                    let identifier = volume.get_identifier('class');
                    let category = identifier && identifier.includes('network') ? 'network' : 'device';
                    this._addMount(category, mount);
                }
            }
        });
        /* add mounts that have no volume (/etc/mtab mounts, ftp, sftp,...) */
        let mounts = this._volumeMonitor.get_mounts();
        mounts.forEach(mount => {
            if (!mount.is_shadowed() && !mount.get_volume()) {
                let location = mount.get_default_location();
                let category = location.is_native() ? 'device' : 'network';
                this._addMount(category, mount);
            }
        });
        this._sort('device');
        this._sort('network');
        super._reload();
    }
};
