/*
 * Copyright (C) 2016-2017 Matthias Klumpp <matthias@tenstral.net>
 *
 * Licensed under the GNU Lesser General Public License Version 3
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the license, or
 * (at your option) any later version.
 *
 * This software 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
 */

module asgen.backends.debian.debpkg;

import std.stdio : File, writeln;
import std.string : format, strip, endsWith, splitLines, join, split;
import std.path : buildNormalizedPath, buildPath, baseName;
import std.array : empty, appender;
import std.file : rmdirRecurse, mkdirRecurse;
import std.typecons : Nullable;
static import std.file;

import asgen.config;
import asgen.logging;
import asgen.zarchive;
import asgen.backends.interfaces;
import asgen.downloader : Downloader;
import asgen.utils : isRemote;

/**
 * Helper class for simple deduplication of package descriptions
 * between packages of different architectures in memory.
 */
final class DebPackageLocaleTexts {
    string[string] summary; /// map of localized package short summaries
    string[string] description; /// map of localized package descriptions

    void setDescription (string text, string locale)
    {
        synchronized (this)
            description[locale] = text;
    }

    void setSummary (string text, string locale)
    {
        synchronized (this)
            summary[locale] = text;
    }
}

/**
 * Representation of a Debian binary package
 */
class DebPackage : Package {
private:
    string pkgname;
    string pkgver;
    string pkgarch;
    string pkgmaintainer;
    DebPackageLocaleTexts descTexts;
    Nullable!GStreamer gstreamer;

    bool contentsRead;
    string[] contentsL;

    string tmpDir;
    ArchiveDecompressor controlArchive;
    ArchiveDecompressor dataArchive;

    string debFname;
    string localDebFname;

public:
    final @property override string name () const
    {
        return pkgname;
    }

    final @property override string ver () const
    {
        return pkgver;
    }

    final @property override string arch () const
    {
        return pkgarch;
    }

    final @property void name (string s)
    {
        pkgname = s;
    }

    final @property void ver (string s)
    {
        pkgver = s;
    }

    final @property void arch (string s)
    {
        pkgarch = s;
    }

    final @property override Nullable!GStreamer gst ()
    {
        return gstreamer;
    }

    final @property void gst (GStreamer gst)
    {
        gstreamer = gst;
    }

    final @property override const(string[string]) description () const
    {
        return descTexts.description;
    }

    final @property override const(string[string]) summary () const
    {
        return descTexts.summary;
    }

    final @property void filename (string fname)
    {
        debFname = fname;
        localDebFname = null;
    }

    override final
    string getFilename ()
    {
        if (!localDebFname.empty)
            return localDebFname;

        if (debFname.isRemote) {
            synchronized (this) {
                auto dl = Downloader.get;
                immutable path = buildNormalizedPath(tmpDir, debFname.baseName);
                dl.downloadFile(debFname, path);
                localDebFname = path;
                return localDebFname;
            }
        } else {
            localDebFname = debFname;
            return debFname;
        }
    }

    override
    final @property string maintainer () const
    {
        return pkgmaintainer;
    }

    final @property void maintainer (string maint)
    {
        pkgmaintainer = maint;
    }

    this (string pname, string pver, string parch, DebPackageLocaleTexts l10nTexts = null)
    {
        pkgname = pname;
        pkgver = pver;
        pkgarch = parch;

        descTexts = l10nTexts;
        if (descTexts is null)
            descTexts = new DebPackageLocaleTexts;

        contentsRead = false;

        updateTmpDirPath();
    }

    ~this ()
    {
        // FIXME: We can't properly clean up because we can't GC-allocate in a destructor (leads to crashes),
        // see if this is fixed in a future version of D, or simply don't use the GC in close ().
        // close ();
    }

    final void updateTmpDirPath ()
    {
        auto conf = Config.get();
        tmpDir = buildPath(conf.getTmpDir(), format("%s-%s_%s", name, ver, arch));
    }

    final void setDescription (string text, string locale)
    {
        descTexts.setDescription(text, locale);
    }

    final void setSummary (string text, string locale)
    {
        descTexts.setSummary(text, locale);
    }

    final setLocalizedTexts(DebPackageLocaleTexts l10nTexts)
    {
        assert(l10nTexts !is null);
        descTexts = l10nTexts;
    }

    @property
    final DebPackageLocaleTexts localizedTexts ()
    {
        return descTexts;
    }

    private auto openPayloadArchive()
    {
        import std.regex : ctRegex;

        synchronized (this) {
            if (dataArchive.isOpen)
                return dataArchive;

            ArchiveDecompressor ad;
            // extract the payload to a temporary location first
            ad.open(this.getFilename);
            mkdirRecurse(tmpDir);

            auto files = ad.extractFilesByRegex(ctRegex!(r"data\.*"), tmpDir);
            if (files.length == 0)
                throw new Exception("Unable to find the payload tarball in Debian package: %s".format(this.getFilename));
            immutable dataArchiveFname = files[0];

            dataArchive.open(dataArchiveFname);
            return dataArchive;
        }
    }

    final void extractPackage (const string dest = buildPath(tmpDir, name))
    {
        import std.file : exists;
        import std.regex : ctRegex;

        synchronized (this) {
            if (!dest.exists)
                mkdirRecurse (dest);

            auto pa = openPayloadArchive();
            pa.extractArchive(dest);
        }
    }

    private final auto openControlArchive()
    {
        import std.regex : ctRegex;

        synchronized (this) {
            if (controlArchive.isOpen)
                return controlArchive;

            ArchiveDecompressor ad;
            // extract the payload to a temporary location first
            ad.open(this.getFilename);
            mkdirRecurse(tmpDir);

            auto files = ad.extractFilesByRegex(ctRegex!(r"control\.*"), tmpDir);
            if (files.empty)
                throw new Exception("Unable to find control data in Debian package: %s".format(this.getFilename));
            immutable controlArchiveFname = files[0];

            controlArchive.open(controlArchiveFname);
            return controlArchive;
        }
    }

    override final
    const(ubyte)[] getFileData (string fname)
    {
        auto pa = openPayloadArchive();
        return pa.readData(fname);
    }

    @property override final
    string[] contents ()
    {
        import std.utf : toUTF8;

        if (contentsRead)
            return contentsL;

        if (pkgname.endsWith("icon-theme")) {
            // the md5sums file does not contain symbolic links - while that is okay-ish for regular
            // packages, it is not acceptable for icon themes, since those rely on symlinks to provide
            // aliases for certain icons. So, use the slow method for reading contents information here.

            auto pa = openPayloadArchive();
            contentsL = pa.readContents();
            contentsRead = true;

            return contentsL;
        }

        // use the md5sums file of the .deb control archive to determine
        // the contents of this package.
        // this is way faster than going through the payload directly, and
        // has the same accuracy.
        auto ca = openControlArchive();
        const(ubyte)[] md5sumsData;
        try {
            md5sumsData = ca.readData("./md5sums");
        } catch (Exception e) {
            logWarning("Could not read md5sums file for package %s: %s", this.id, e.msg);
            return [];
        }

        auto md5sums = cast(string) md5sumsData;
        try {
            md5sums = md5sums.toUTF8;
        } catch (Exception e) {
            logError("Could not decode md5sums file for package %s: %s", this.id, e.msg);
            return [];
        }

        auto contentsAppender = appender!(string[]);
        contentsAppender.reserve(20);
        foreach (line; md5sums.splitLines()) {
            auto parts = line.split("  ");
            if (parts.length <= 0)
                continue;
            string c = join(parts[1 .. $], "  ");
            contentsAppender.put("/" ~ c);
        }
        contentsL = contentsAppender.data;

        contentsRead = true;
        return contentsL;
    }

    /**
     * Get the "control" file information from the control archive
     * in the Debian package.
     * This is useful to get information from a package directly, e.g.
     * for processing a single package.
     */
    final auto readControlInformation()
    {
        import std.utf : toUTF8;
        import asgen.backends.debian.tagfile : TagFile;

        auto ca = openControlArchive();
        const(ubyte)[] controlData;
        try {
            controlData = ca.readData("./control");
        } catch (Exception e) {
            logError("Could not read control file for package %s: %s", this.id, e.msg);
            return null;
        }

        auto controlStr = cast(string) controlData;
        try {
            controlStr = controlStr.toUTF8;
        } catch (Exception e) {
            logError("Could not decode control file for package %s: %s", this.id, e.msg);
            return null;
        }

        auto tf = new TagFile();
        tf.load(controlStr);
        return tf;
    }

    override final
    void cleanupTemp ()
    {
        synchronized (this) {
            if (controlArchive.isOpen)
                controlArchive.close();
            if (dataArchive.isOpen)
                dataArchive.close();

            try {
                if (std.file.exists(tmpDir)) {
                    /* Whenever we delete the temporary directory, we need to
                     * forget about the local file too, since (if it's remote) that
                     * was downloaded into there. */
                    logDebug("Deleting temporary directory %s", tmpDir);
                    localDebFname = null;
                    rmdirRecurse(tmpDir);
                }
            } catch (Exception e) {
                // we ignore any error
                logDebug("Unable to remove temporary directory: %s (%s)", tmpDir, e.msg);
            }
        }
    }

    override final
    void finish ()
    {
        cleanupTemp();
    }
}
