/* DING: Desktop Icons New Generation for GNOME Shell
 *
 * Copyright (C) 2022, 2024 Sundeep Mediratta (smedius@gmail.com) port to work with
 * gnome desktop 4
 *
 * Code cherry picked from Marco Trevisan for async methods to generate icons.
 *
 * Copyright (C) 2021 Sergio Costas (rastersoft@gmail.com)
 *
 * 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, version 3 of the License.
 *
 * 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/>.
 */
import {Cairo, Gdk, GdkPixbuf, GLib, Gio, GnomeDesktop, Poppler} from
    '../dependencies/gi.js';

export {ThumbnailLoader};

Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype,
    'generate_thumbnail_async',
    'generate_thumbnail_finish');
Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype,
    'create_failed_thumbnail_async',
    'create_failed_thumbnail_finish');
Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype,
    'save_thumbnail_async',
    'save_thumbnail_finish');

const PIXBUF_CONTENT_TYPES = new Set();

GdkPixbuf.Pixbuf
.get_formats().forEach(f => PIXBUF_CONTENT_TYPES.add(...f.get_mime_types()));

// Max file size for which to attempt thumbnail generation with local code
const MAX_FILE_SIZE = 5242880;

// Width and height of icons generated by local code
const WIDTH = 130;
const HEIGHT = 130;

const ThumbnailLoader = class {
    constructor(FileUtils) {
        this.FileUtils = FileUtils;
        this._timeoutValue = 5000;

        this._thumbnailFactory =
            GnomeDesktop.DesktopThumbnailFactory
            .new(GnomeDesktop.DesktopThumbnailSize.LARGE);

        this.standardThumbnailsFolder =
            GLib.build_filenamev([GLib.get_home_dir(), '.cache/thumbnails']);

        this.standardThumbnailSubFolders = ['large', 'normal'];

        this.gimpSnapThumbnailsFolder =
            GLib.build_filenamev(
                [
                    GLib.get_home_dir(),
                    'snap/common/gimp',
                    '.cache/thumbnails',
                ]
            );

        this.gimpFlatPackThumbnailsFolder =
            GLib.build_filenamev(
                [
                    GLib.get_home_dir(),
                    '.var/app/org.gimp.GIMP',
                    'cache/thumbnails',
                ]
            );

        this.md5Hasher = GLib.Checksum.new(GLib.ChecksumType.MD5);
        this.textCoder = new TextEncoder();
    }

    async _generateThumbnail(file, cancellable) {
        if (!await this.FileUtils.queryExists(file.file))
            return null;

        if (this._thumbnailFactory
            .has_valid_failed_thumbnail(file.uri, file.modifiedTime)
        )
            return null;

        if (!await this._createThumbnailAsync(file, cancellable)) {
            await this._createFailedThumbnailAsync(file, cancellable);
            return null;
        }

        if (cancellable.is_cancelled())
            return null;

        return this._thumbnailFactory.lookup(file.uri, file.modifiedTime);
    }

    async _createThumbnailAsync(file, cancellable) {
        let gotTimeout = false;

        let timeoutId =
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._timeoutValue, () => {
                console.log(
                    `Timeout while generating thumbnail for ${file.displayName}`
                );

                timeoutId = 0;
                gotTimeout = true;
                cancellable.cancel();

                return GLib.SOURCE_REMOVE;
            });

        try {
            const thumbnailPixbuf =
                await this._thumbnailFactory
                .generate_thumbnail_async(
                    file.uri,
                    file.attributeContentType,
                    cancellable
                );

            await this._thumbnailFactory
                .save_thumbnail_async(
                    thumbnailPixbuf,
                    file.uri,
                    file.modifiedTime,
                    cancellable
                )
                .catch(e => {
                    if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                        console.error(`Error saving thumbnail ${e}`);
                });

            return true;
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
                console.error(e,
                    'Error creating thumbnail with thumbnailFactory: ' +
                    `${e.message}`
                );
            }

            return await this._createFallBackThumbnailAsync(
                file,
                gotTimeout && cancellable.is_cancelled() ? null : cancellable
            );
        } finally {
            if (timeoutId)
                GLib.source_remove(timeoutId);
        }
    }

    async _createFallBackThumbnailAsync(file, cancellable) {
        const thumbnailPixbuf = this._createThumbnailLocally(file, cancellable);

        if (thumbnailPixbuf !== null) {
            await this._thumbnailFactory
                .save_thumbnail_async(
                    thumbnailPixbuf,
                    file.uri,
                    file.modifiedTime,
                    cancellable
                )
                .catch(e => {
                    if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                        console.error(`Error saving thumbnail ${e}`);
                });

            return true;
        }

        return false;
    }

    async _createFailedThumbnailAsync(file, cancellable) {
        try {
            await this._thumbnailFactory.create_failed_thumbnail_async(
                file.uri,
                file.modifiedTime,
                cancellable
            );
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
                console.error(e,
                    `Error while creating failed thumbnail: ${e.message}`
                );
            }
        }
    }

    _createThumbnailLocally(file, cancellable) {
        let thumbnailPixbuf = null;

        if (file.fileSize < MAX_FILE_SIZE) {
            const contentType = file.attributeContentType;

            try {
                if (PIXBUF_CONTENT_TYPES.has(contentType))
                    thumbnailPixbuf = this._loadImageAsIcon(file, cancellable);
                else if (
                    contentType === 'application/pdf' ||
                    contentType === 'x-pdf'
                )
                    thumbnailPixbuf = this._loadPdfAsIcon(file, cancellable);
            } catch (e) {
                if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                    throw e;

                console.error(e,
                    `Error while generating icon image: ${e.message}`
                );
            }
        }

        return thumbnailPixbuf;
    }

    _loadPdfAsIcon(file, cancellable) {
        let thumbnailPixbuf = null;

        try {
            // Assume no password
            const password = null;

            const popplerDocument =
                Poppler.Document.new_from_gfile(
                    file.file,
                    password,
                    cancellable
                );

            if (!popplerDocument)
                return thumbnailPixbuf;

            const firstPage = popplerDocument.get_page(0);

            if (!firstPage)
                return thumbnailPixbuf;

            const [pagewidth, pageheight] = firstPage.get_size();

            let width = WIDTH;
            let height = HEIGHT;
            const aspectRatio = pagewidth / pageheight;

            if ((width / height) > aspectRatio)
                width = height * aspectRatio;
            else
                height = width / aspectRatio;

            const hScale = width / pagewidth;
            const vScale = height / pageheight;

            const imageSurface =
                new Cairo.ImageSurface(
                    Cairo.Format.ARGB32,
                    pagewidth,
                    pageheight
                );

            const ctx = new Cairo.Context(imageSurface);

            this._drawPdfOn(ctx, firstPage);

            const scaledSurface =
                new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);

            const scaledCtx = new Cairo.Context(scaledSurface);
            scaledCtx.scale(hScale, vScale);

            scaledCtx.setSourceSurface(imageSurface, 0, 0);
            scaledCtx.paint();

            thumbnailPixbuf =
                Gdk.pixbuf_get_from_surface(scaledSurface, 0, 0, width, height);

            ctx.$dispose();
            scaledCtx.$dispose();
        } catch (e) {
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                throw e;

            if (!e.matches(Poppler.Error, Poppler.Error.ENCRYPTED)) {
                console.error(e,
                    `Error creating pdf thumbnail pixbuf ${file.uri}`
                );
            }
        }

        return thumbnailPixbuf;
    }

    _drawPdfOn(ctx, firstPage) {
        ctx.setSourceRGBA(1, 1, 1, 1);
        ctx.save();
        ctx.paint();
        ctx.restore();
        firstPage.render(ctx);
        ctx.save();
    }

    _loadImageAsIcon(file) {
        let thumbnailPixbuf = null;

        try {
            const pixbuf = GdkPixbuf.Pixbuf.new_from_file(file.path);
            let width = WIDTH;
            let height = HEIGHT;
            const aspectRatio = pixbuf.width / pixbuf.height;

            if ((width / height) > aspectRatio)
                width = height * aspectRatio;
            else
                height = width / aspectRatio;

            thumbnailPixbuf =
                pixbuf.scale_simple(
                    width,
                    height,
                    GdkPixbuf.InterpType.BILINEAR
                );

            return thumbnailPixbuf;
        } catch (e) {
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                throw e;

            console.error(e,
                `Error creating image thumbnail pixbuf ${file.uri}`
            );
        }

        return thumbnailPixbuf;
    }

    /*
     * ExtraCode to find thumbnail in the thumbnail Folder
     * Was Used to find GIMP thumbnails, however ThumbnailFactoryNormal
     * can now find it.
     *
     * However to do that you have to start two ThumbnailFactories,
     * this is simpler and lighter, can be used to search arbitrary folders
     * for thumbnails in Futre if necessary, not just Subfolders
    */

    _findThumbnail(file, basePath, subFolders = null, cancellable) {
        if (!basePath)
            return null;

        let md5FileUriHash = this._getMD5Hash(file.uri);

        if (!md5FileUriHash)
            return null;

        let thumbnailMD5Name = `${md5FileUriHash}.png`;
        let thumbnailFilePath = null;
        let thumbnailFileSearchPath = null;

        if (subFolders) {
            for (const subfolder of subFolders) {
                thumbnailFileSearchPath =
                    GLib
                    .build_filenamev([basePath, subfolder, thumbnailMD5Name]);

                if (
                    Gio.File.new_for_path(thumbnailFileSearchPath)
                    .query_exists(cancellable)
                ) {
                    thumbnailFilePath = thumbnailFileSearchPath;

                    break;
                }
            }
            return thumbnailFilePath;
        }

        thumbnailFileSearchPath =
            GLib.build_filenamev([basePath, thumbnailMD5Name]);

        if (
            Gio.File.new_for_path(thumbnailFileSearchPath)
            .query_exists(cancellable)
        )
            thumbnailFilePath = thumbnailFileSearchPath;

        return thumbnailFilePath;
    }

    _getMD5Hash(string) {
        let hashString = null;
        this.md5Hasher.update(this.textCoder.encode(string));
        hashString = this.md5Hasher.get_string();
        this.md5Hasher.reset();
        return hashString;
    }

    canThumbnail(file) {
        return this._thumbnailFactory
            .can_thumbnail(
                file.uri,
                file.attributeContentType,
                file.modifiedTime
            );
    }

    _lookupThumbnail(file, cancellable) {
        let thumbnail = null;

        // do searches for only special cases to conserve resources //
        if (file.attributeContentType === 'image/x-xcf') {
            // lets do a local search in thumbnails dir, look only in normal
            // subfolder as we already searched large
            thumbnail =
                this._findThumbnail(
                    file,
                    this.standardThumbnailsFolder,
                    ['normal'],
                    cancellable
                );

            if (thumbnail)
                return thumbnail;

            // we can now search far and wide in snaps and flatpacks if we want.
            thumbnail =
                this._findThumbnail(
                    file,
                    this.gimpSnapThumbnailsFolder,
                    this.standardThumbnailSubFolders,
                    cancellable
                );

            if (!thumbnail) {
                thumbnail =
                    this._findThumbnail(
                        file,
                        this.gimpFlatPackThumbnailsFolder,
                        this.standardThumbnailSubFolders,
                        cancellable
                    );
            }

            return thumbnail;
        }

        return thumbnail;
    }

    hasThumbnail(file, cancellable) {
        let thumbnail = null;

        thumbnail = this._thumbnailFactory.lookup(file.uri, file.modifiedTime);

        if (thumbnail)
            return thumbnail;

        thumbnail = this._lookupThumbnail(file, cancellable);

        return thumbnail;
    }

    async getThumbnail(file, cancellable) {
        try {
            let thumbnail = this.hasThumbnail(file, cancellable);

            if (!thumbnail && this.canThumbnail(file))
                thumbnail = await this._generateThumbnail(file, cancellable);

            if (
                !thumbnail &&
                await this._createFallBackThumbnailAsync(file, cancellable)
            ) {
                thumbnail =
                    this._thumbnailFactory.lookup(file.uri, file.modifiedTime);
            }

            return thumbnail;
        } catch (error) {
            console.log(
                `Error when asking for a thumbnail for ${file.displayName}:` +
                ` ${error.message}\n${error.stack}`);
        }

        return null;
    }
};
