package org.fdroid.fdroid;

import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Process;
import android.os.SystemClock;

import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.installer.ApkCache;

import java.io.File;
import java.util.concurrent.TimeUnit;

/**
 * Handles cleaning up caches files that are not going to be used, and do not
 * block the operation of the app itself.  For things that must happen before
 * F-Droid starts normal operation, that should go into
 * {@link FDroidApp#onCreate()}.
 * <p>
 * These files should only be deleted when they are at least an hour-ish old,
 * in case they are actively in use while {@code CleanCacheService} is running.
 * {@link #clearOldFiles(File, long)} checks the file age using access time from
 * {@link android.system.StructStat#st_atime} on {@link android.os.Build.VERSION_CODES#LOLLIPOP}
 * and newer.  On older Android, last modified time from {@link File#lastModified()}
 * is used.
 */
public class CleanCacheService extends IntentService {

    /**
     * Schedule or cancel this service to update the app index, according to the
     * current preferences. Should be called a) at boot, b) if the preference
     * is changed, or c) on startup, in case we get upgraded.
     */
    public static void schedule(Context context) {
        long keepTime = Preferences.get().getKeepCacheTime();
        long interval = TimeUnit.DAYS.toMillis(1);
        if (keepTime < interval) {
            interval = keepTime;
        }

        Intent intent = new Intent(context, CleanCacheService.class);
        PendingIntent pending = PendingIntent.getService(context, 0, intent, 0);

        AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        alarm.cancel(pending);
        alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
                SystemClock.elapsedRealtime() + 5000, interval, pending);
    }

    public CleanCacheService() {
        super("CleanCacheService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent == null) {
            return;
        }
        Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
        deleteExpiredApksFromCache();
        deleteStrayIndexFiles();
        deleteOldInstallerFiles();
        deleteOldIcons();
    }

    /**
     * All downloaded APKs will be cached for a certain amount of time, which is
     * specified by the user in the "Keep Cache Time" preference.  This removes
     * any APK in the cache that is older than that preference specifies.
     */
    private void deleteExpiredApksFromCache() {
        File cacheDir = ApkCache.getApkCacheDir(getBaseContext());
        clearOldFiles(cacheDir, Preferences.get().getKeepCacheTime());
    }

    /**
     * {@link org.fdroid.fdroid.installer.Installer} instances copy the APK into
     * a safe place before installing.  It doesn't clean up them reliably yet.
     */
    private void deleteOldInstallerFiles() {
        File filesDir = getFilesDir();
        if (filesDir == null) {
            return;
        }

        final File[] files = filesDir.listFiles();
        if (files == null) {
            return;
        }

        for (File f : files) {
            if (f.getName().startsWith("install-")) {
                clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
            }
        }
    }

    /**
     * Delete index files which were downloaded, but not removed (e.g. due to F-Droid being
     * force closed during processing of the file, before getting a chance to delete). This
     * may include both "index-*-downloaded" and "index-*-extracted.xml" files.
     * <p>
     * Note that if the SD card is not ready, then the cache directory will probably not be
     * available. In this situation no files will be deleted (and thus they may still exist
     * after the SD card becomes available).
     * <p>
     * This also deletes temp files that are created by
     * {@link org.fdroid.fdroid.net.DownloaderFactory#create(Context, String)}, e.g. "dl-*"
     */
    private void deleteStrayIndexFiles() {
        File cacheDir = getCacheDir();
        if (cacheDir == null) {
            return;
        }

        final File[] files = cacheDir.listFiles();
        if (files == null) {
            return;
        }

        for (File f : files) {
            if (f.getName().startsWith("index-")) {
                clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
            }
            if (f.getName().startsWith("dl-")) {
                clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
            }
        }
    }

    /**
     * Delete cached icons that have not been accessed in over a year.
     */
    private void deleteOldIcons() {
        clearOldFiles(Utils.getIconsCacheDir(this), TimeUnit.DAYS.toMillis(365));
    }

    /**
     * Recursively delete files in {@code f} that were last used
     * {@code millisAgo} milliseconds ago.  On {@code android-21} and newer, this
     * is based on the last access of the file, on older Android versions, it is
     * based on the last time the file was modified, e.g. downloaded.
     *
     * @param f         The file or directory to clean
     * @param millisAgo The number of milliseconds old that marks a file for deletion.
     */
    public static void clearOldFiles(File f, long millisAgo) {
        if (f == null) {
            return;
        }
        long olderThan = System.currentTimeMillis() - millisAgo;
        if (f.isDirectory()) {
            File[] files = f.listFiles();
            if (files == null) {
                return;
            }
            for (File file : files) {
                clearOldFiles(file, millisAgo);
            }
            f.delete();
        } else if (Build.VERSION.SDK_INT < 21) {
            if (FileUtils.isFileOlder(f, olderThan)) {
                f.delete();
            }
        } else {
            CleanCacheService21.deleteIfOld(f, olderThan);
        }
    }
}