/*
 * Copyright (c) 2015-present, Parse, LLC.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
package com.parse;

import android.content.Context;

import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;

/**
 * Used for ParseQuery caching.
 */
/** package */ class ParseKeyValueCache {

  private static final String TAG = "ParseKeyValueCache";
  private static final String DIR_NAME = "ParseKeyValueCache";

  // We limit the cache to 2MB because that's about what the default browser
  // uses.
  /* package */ static final int DEFAULT_MAX_KEY_VALUE_CACHE_BYTES = 2 * 1024 * 1024;
  // We limit to 1000 cache files to avoid taking too long while scanning the
  // cache
  /* package */ static final int DEFAULT_MAX_KEY_VALUE_CACHE_FILES = 1000;

  /**
   * Prevent multiple threads from modifying the cache at the same time.
   */
  private static final Object MUTEX_IO = new Object();

  /* package */ static int maxKeyValueCacheBytes = DEFAULT_MAX_KEY_VALUE_CACHE_BYTES;
  /* package */ static int maxKeyValueCacheFiles = DEFAULT_MAX_KEY_VALUE_CACHE_FILES;

  private static File directory;

  // Creates a directory to keep cache-type files in.
  // The operating system will automatically clear out these files first
  // when space gets low.
  /* package */ static void initialize(Context context) {
    initialize(new File(context.getCacheDir(), DIR_NAME));
  }

  /* package for tests */ static void initialize(File path) {
    if (!path.isDirectory() && !path.mkdir()) {
      throw new RuntimeException("Could not create ParseKeyValueCache directory");
    }
    directory = path;
  }

  private static File getKeyValueCacheDir() {
    if (directory == null || !directory.exists()) {
      directory.mkdir();
    }
    return directory;
  }

  /**
   * How many files are in the key-value cache.
   */
  /* package */ static int size() {
    File[] files = getKeyValueCacheDir().listFiles();
    if (files == null) {
      return 0;
    }
    return files.length;
  }

  private static File getKeyValueCacheFile(String key) {
    final String suffix = '.' + key;
    File[] matches = getKeyValueCacheDir().listFiles(new FilenameFilter() {
      @Override
      public boolean accept(File dir, String filename) {
        return filename.endsWith(suffix);
      }
    });
    return (matches == null || matches.length == 0) ? null : matches[0];
  }

  // Badly formatted files return the epoch
  private static long getKeyValueCacheAge(File cacheFile) {
    // Format: <date>.<key>
    String name = cacheFile.getName();
    try {
      return Long.parseLong(name.substring(0, name.indexOf('.')));
    } catch (NumberFormatException e) {
      return 0;
    }
  }

  private static File createKeyValueCacheFile(String key) {
    String filename = String.valueOf(new Date().getTime()) + '.' + key;
    return new File(getKeyValueCacheDir(), filename);
  }

  // Removes all the cache entries.
  /* package */ static void clearKeyValueCacheDir() {
    synchronized (MUTEX_IO) {
      File dir = getKeyValueCacheDir();
      if (dir == null) {
        return;
      }
      File[] entries = dir.listFiles();
      if (entries == null) {
        return;
      }
      for (File entry : entries) {
        entry.delete();
      }
    }
  }

  // Saves a key-value pair to the cache
  /* package */ static void saveToKeyValueCache(String key, String value) {
    synchronized (MUTEX_IO) {
      File prior = getKeyValueCacheFile(key);
      if (prior != null) {
        prior.delete();
      }
      File f = createKeyValueCacheFile(key);
      try {
        ParseFileUtils.writeByteArrayToFile(f, value.getBytes("UTF-8"));
      } catch (UnsupportedEncodingException e) {
        // do nothing
      } catch (IOException e) {
        // do nothing
      }

      // Check if we should kick out old cache entries
      File[] files = getKeyValueCacheDir().listFiles();
      // We still need this check since dir.mkdir() may fail
      if (files == null || files.length == 0) {
        return;
      }

      int numFiles = files.length;
      int numBytes = 0;
      for (File file : files) {
        numBytes += file.length();
      }

      // If we do not need to clear the cache, simply return
      if (numFiles <= maxKeyValueCacheFiles && numBytes <= maxKeyValueCacheBytes) {
        return;
      }

      // We need to kick out some cache entries.
      // Sort oldest-first. We touch on read so mtime is really LRU.
      // Sometimes (i.e. tests) the time of lastModified isn't granular enough,
      // so we resort
      // to sorting by the file name which is always prepended with time in ms
      Arrays.sort(files, new Comparator<File>() {
        @Override
        public int compare(File f1, File f2) {
          int dateCompare = Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
          if (dateCompare != 0) {
            return dateCompare;
          } else {
            return f1.getName().compareTo(f2.getName());
          }
        }
      });

      for (File file : files) {
        numFiles--;
        numBytes -= file.length();
        file.delete();

        if (numFiles <= maxKeyValueCacheFiles && numBytes <= maxKeyValueCacheBytes) {
          break;
        }
      }
    }
  }

  // Clears a key from the cache if it's there. If it's not there, this is a
  // no-op.
  /* package */ static void clearFromKeyValueCache(String key) {
    synchronized (MUTEX_IO) {
      File file = getKeyValueCacheFile(key);
      if (file != null) {
        file.delete();
      }
    }
  }

  // Loads a value from the key-value cache.
  // Returns null if nothing is there.
  /* package */ static String loadFromKeyValueCache(final String key, final long maxAgeMilliseconds) {
    synchronized (MUTEX_IO) {
      File file = getKeyValueCacheFile(key);
      if (file == null) {
        return null;
      }

      Date now = new Date();
      long oldestAcceptableAge = Math.max(0, now.getTime() - maxAgeMilliseconds);
      if (getKeyValueCacheAge(file) < oldestAcceptableAge) {
        return null;
      }

      // Update mtime to make the LRU work
      file.setLastModified(now.getTime());

      try {
        RandomAccessFile f = new RandomAccessFile(file, "r");
        byte[] bytes = new byte[(int) f.length()];
        f.readFully(bytes);
        f.close();
        return new String(bytes, "UTF-8");
      } catch (IOException e) {
        PLog.e(TAG, "error reading from cache", e);
        return null;
      }
    }
  }

  // Returns null if the value does not exist or is not json
  /* package */ static JSONObject jsonFromKeyValueCache(String key, long maxAgeMilliseconds) {
    String raw = loadFromKeyValueCache(key, maxAgeMilliseconds);
    if (raw == null) {
      return null;
    }

    try {
      return new JSONObject(raw);
    } catch (JSONException e) {
      PLog.e(TAG, "corrupted cache for " + key, e);
      clearFromKeyValueCache(key);
      return null;
    }
  }
}

@@ -2166,26 +2166,26 @@
 rectory 
-=
+!
 = null 
-%7C%7C
+&&
  !direct

Fix NPE