content: package net.nullsum.audinaut.util;
  
  import android.content.Context;
  import android.os.StatFs;
  import android.util.Log;
  
  import net.nullsum.audinaut.domain.Playlist;
  import net.nullsum.audinaut.service.DownloadFile;
  import net.nullsum.audinaut.service.DownloadService;
  import net.nullsum.audinaut.service.MediaStoreService;
  
  import java.io.File;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  import java.util.SortedSet;
  
  /**
   * @author Sindre Mehus
   * @version $Id$
   */
  public class CacheCleaner {
  
      private static final String TAG = CacheCleaner.class.getSimpleName();
      private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
      private static final long MAX_COVER_ART_SPACE = 100 * 1024L * 1024L;
  
      private final Context context;
      private final DownloadService downloadService;
      private final MediaStoreService mediaStore;
  
      public CacheCleaner(Context context, DownloadService downloadService) {
          this.context = context;
          this.downloadService = downloadService;
          this.mediaStore = new MediaStoreService(context);
      }
  
      public void clean() {
          new BackgroundCleanup(context).execute();
      }
  
      public void cleanSpace() {
          new BackgroundSpaceCleanup(context).execute();
      }
  
      public void cleanPlaylists(List<Playlist> playlists) {
          new BackgroundPlaylistsCleanup(context, playlists).execute();
      }
  
      private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) {
          for (File dir : dirs) {
              if (undeletable.contains(dir)) {
                  continue;
              }
  
              FileUtil.deleteEmptyDir(dir);
          }
      }
  
      private long getMinimumDelete(List<File> files, List<File> pinned) {
          if (files.size() == 0) {
              return 0L;
          }
  
          long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L;
  
          long bytesUsedBySubsonic = 0L;
          for (File file : files) {
              bytesUsedBySubsonic += file.length();
          }
          for (File file : pinned) {
              bytesUsedBySubsonic += file.length();
          }
  
          // Ensure that file system is not more than 95% full.
          StatFs stat = new StatFs(files.get(0).getPath());
          long bytesTotalFs = stat.getBlockCountLong() * stat.getBlockSizeLong();
          long bytesAvailableFs = stat.getAvailableBlocksLong() * stat.getBlockSizeLong();
          long bytesUsedFs = bytesTotalFs - bytesAvailableFs;
          long minFsAvailability = bytesTotalFs - MIN_FREE_SPACE;
  
          long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L);
          long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
          long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit);
  
          Log.i(TAG, "File system       : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available");
          Log.i(TAG, "Cache limit       : " + Util.formatBytes(cacheSizeBytes));
          Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic));
          Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
  
          return bytesToDelete;
      }
  
      private void deleteFiles(List<File> files, Set<File> undeletable, long bytesToDelete, boolean deletePartials) {
          if (files.isEmpty()) {
              return;
          }
  
          long bytesDeleted = 0L;
          for (File file : files) {
              if (!deletePartials && bytesDeleted > bytesToDelete) break;
  
              if (bytesToDelete > bytesDeleted || (deletePartials && (file.getName().endsWith(".partial") || file.getName().contains(".partial.")))) {
                  if (!undeletable.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE)) {
                      long size = file.length();
                      if (Util.delete(file)) {
                          bytesDeleted += size;
                          mediaStore.deleteFromMediaStore(file);
                      }
                  }
              }
          }
  
          Log.i(TAG, "Deleted           : " + Util.formatBytes(bytesDeleted));
      }
  
      private void findCandidatesForDeletion(File file, List<File> files, List<File> pinned, List<File> dirs) {
          if (file.isFile()) {
              String name = file.getName();
              boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
              if (isCacheFile) {
                  files.add(file);
              } else {
                  pinned.add(file);
              }
          } else {
              // Depth-first
              for (File child : FileUtil.listFiles(file)) {
                  findCandidatesForDeletion(child, files, pinned, dirs);
              }
              dirs.add(file);
          }
      }
  
      private void sortByAscendingModificationTime(List<File> files) {
          Collections.sort(files, (a, b) -> Long.compare(a.lastModified(), b.lastModified()));
      }
  
      private Set<File> findUndeletableFiles() {
          Set<File> undeletable = new HashSet<>(5);
  
          for (DownloadFile downloadFile : downloadService.getDownloads()) {
              undeletable.add(downloadFile.getPartialFile());
              undeletable.add(downloadFile.getCompleteFile());
          }
  
          undeletable.add(FileUtil.getMusicDirectory(context));
          return undeletable;
      }
  
      private void cleanupCoverArt(Context context) {
          File dir = FileUtil.getAlbumArtDirectory(context);
  
          List<File> files = new ArrayList<>();
          long bytesUsed = 0L;
          for (File file : dir.listFiles()) {
              if (file.isFile()) {
                  files.add(file);
                  bytesUsed += file.length();
              }
          }
  
          // Don't waste time sorting if under limit already
          if (bytesUsed < MAX_COVER_ART_SPACE) {
              return;
          }
  
          sortByAscendingModificationTime(files);
          long bytesDeleted = 0L;
          for (File file : files) {
              // End as soon as the space used is below the threshold
              if (bytesUsed < MAX_COVER_ART_SPACE) {
                  break;
              }
  
              long bytes = file.length();
              if (file.delete()) {
                  bytesUsed -= bytes;
                  bytesDeleted += bytes;
              }
          }
  
          Log.i(TAG, "Deleted " + Util.formatBytes(bytesDeleted) + " worth of cover art");
      }
  
      private class BackgroundCleanup extends SilentBackgroundTask<Void> {
          public BackgroundCleanup(Context context) {
              super(context);
          }
  
          @Override
          protected Void doInBackground() {
              if (downloadService == null) {
                  Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
                  return null;
              }
  
              try {
                  List<File> files = new ArrayList<>();
                  List<File> pinned = new ArrayList<>();
                  List<File> dirs = new ArrayList<>();
  
                  findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs);
                  sortByAscendingModificationTime(files);
  
                  Set<File> undeletable = findUndeletableFiles();
  
                  deleteFiles(files, undeletable, getMinimumDelete(files, pinned), true);
                  deleteEmptyDirs(dirs, undeletable);
  
                  // Make sure cover art directory does not grow too large
                  cleanupCoverArt(context);
              } catch (RuntimeException x) {
                  Log.e(TAG, "Error in cache cleaning.", x);
              }
  
              return null;
          }
      }
  
      private class BackgroundSpaceCleanup extends SilentBackgroundTask<Void> {
          public BackgroundSpaceCleanup(Context context) {
              super(context);
          }
  
          @Override
          protected Void doInBackground() {
              if (downloadService == null) {
                  Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
                  return null;
              }
  
              try {
                  List<File> files = new ArrayList<>();
                  List<File> pinned = new ArrayList<>();
                  List<File> dirs = new ArrayList<>();
                  findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs);
  
                  long bytesToDelete = getMinimumDelete(files, pinned);
                  if (bytesToDelete > 0L) {
                      sortByAscendingModificationTime(files);
                      Set<File> undeletable = findUndeletableFiles();
                      deleteFiles(files, undeletable, bytesToDelete, false);
                  }
              } catch (RuntimeException x) {
                  Log.e(TAG, "Error in cache cleaning.", x);
              }
  
              return null;
          }
      }
  
      private class BackgroundPlaylistsCleanup extends SilentBackgroundTask<Void> {
          private final List<Playlist> playlists;
  
          public BackgroundPlaylistsCleanup(Context context, List<Playlist> playlists) {
              super(context);
              this.playlists = playlists;
          }
  
          @Override
          protected Void doInBackground() {
              try {
                  String server = Util.getServerName(context);
                  SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(context, server));
                  for (Playlist playlist : playlists) {
                      playlistFiles.remove(FileUtil.getPlaylistFile(context, server, playlist.getName()));
                  }
  
                  for (File playlist : playlistFiles) {
                      playlist.delete();
                  }
              } catch (RuntimeException x) {
                  Log.e(TAG, "Error in playlist cache cleaning.", x);
              }
  
              return null;
          }
      }
  }
  