/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko;

import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserProvider;
import org.mozilla.gecko.home.ImageLoader;
import org.mozilla.gecko.icons.storage.MemoryStorage;
import org.mozilla.gecko.util.ThreadUtils;

import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks2;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

/**
  * This is a utility class to keep track of how much memory and disk-space pressure the
  * system is under. It registers itself as a ComponentCallbacks to receive all onLowMemory()/
  * onTrimMemory() notifications for our app, and also listens for some system intents related
  * to disk-space notifications. Internally it will track how much memory and disk pressure
  * the system is under, and perform various actions to help alleviate the pressure.
  *
  * Note that since there is no notification for when the system has lots of free memory
  * again, this class also assumes that, over time, the system will free up memory. This
  * assumption is implemented using a timer that slowly lowers the internal memory
  * pressure state if no new low-memory notifications are received.
  *
  * Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both
  * of these classes may be accessed from various threads, and have both been designed to
  * be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock
  * is allowed to pick up the MemoryMonitor lock, but not vice-versa.
  */
class MemoryMonitor extends BroadcastReceiver implements ComponentCallbacks2 {
    private static final String LOGTAG = "GeckoMemoryMonitor";
    private static final String ACTION_MEMORY_DUMP = "org.mozilla.gecko.MEMORY_DUMP";
    private static final String ACTION_FORCE_PRESSURE = "org.mozilla.gecko.FORCE_MEMORY_PRESSURE";

    // Memory pressure levels.
    private static final int MEMORY_PRESSURE_NONE = 0;
    private static final int MEMORY_PRESSURE_CLEANUP = 1;
    private static final int MEMORY_PRESSURE_LOW = 2;
    private static final int MEMORY_PRESSURE_MEDIUM = 3;
    private static final int MEMORY_PRESSURE_HIGH = 4;

    // We're living as long as the application, so keeping a static reference to it is okay.
    @SuppressLint("StaticFieldLeak")
    private static final MemoryMonitor sInstance = new MemoryMonitor();

    static MemoryMonitor getInstance() {
        return sInstance;
    }

    private Context mAppContext;
    private final PressureDecrementer mPressureDecrementer;
    private int mMemoryPressure;                  // Synchronized access only.
    private volatile boolean mStoragePressure;    // Accessed via UI thread intent, background runnables.
    private boolean mInited;

    private MemoryMonitor() {
        mPressureDecrementer = new PressureDecrementer();
        mMemoryPressure = MEMORY_PRESSURE_NONE;
    }

    public void init(final Context context) {
        if (mInited) {
            return;
        }

        mAppContext = context.getApplicationContext();
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
        filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
        filter.addAction(ACTION_MEMORY_DUMP);
        filter.addAction(ACTION_FORCE_PRESSURE);
        mAppContext.registerReceiver(this, filter);
        mAppContext.registerComponentCallbacks(this);
        mInited = true;
    }

    @Override
    public void onLowMemory() {
        Log.d(LOGTAG, "onLowMemory() notification received");
        if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) {
            // We need to wait on Gecko here, because if we haven't reduced
            // memory usage enough when we return from this, Android will kill us.
            if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
                GeckoThread.waitOnGecko();
            }
        }
    }

    @Override
    public void onTrimMemory(int level) {
        Log.d(LOGTAG, "onTrimMemory() notification received with level " + level);
        if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
            // We seem to get this just by entering the task switcher or hitting the home button.
            // Seems bogus, because we are the foreground app, or at least not at the end of the LRU list.
            // Just ignore it, and if there is a real memory pressure event (CRITICAL, MODERATE, etc),
            // we'll respond appropriately.
            return;
        }

        switch (level) {
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                // TRIM_MEMORY_MODERATE is the highest level we'll respond to while backgrounded
                increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
                break;
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
                increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM);
                break;
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
                increaseMemoryPressure(MEMORY_PRESSURE_LOW);
                break;
            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
                increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP);
                break;
            default:
                Log.d(LOGTAG, "Unhandled onTrimMemory() level " + level);
                break;
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) {
            Log.d(LOGTAG, "Device storage is low");
            mStoragePressure = true;
            ThreadUtils.postToBackgroundThread(new StorageReducer(context));
        } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
            Log.d(LOGTAG, "Device storage is ok");
            mStoragePressure = false;
        } else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) {
            String label = intent.getStringExtra("label");
            if (label == null) {
                label = "default";
            }
            GeckoAppShell.notifyObservers("Memory:Dump", label);
        } else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) {
            increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
        }
    }

    @WrapForJNI(calledFrom = "ui")
    private static native void dispatchMemoryPressure();

    private boolean increaseMemoryPressure(int level) {
        int oldLevel;
        synchronized (this) {
            // bump up our level if we're not already higher
            if (mMemoryPressure > level) {
                return false;
            }
            oldLevel = mMemoryPressure;
            mMemoryPressure = level;
        }

        Log.d(LOGTAG, "increasing memory pressure to " + level);

        // since we don't get notifications for when memory pressure is off,
        // we schedule our own timer to slowly back off the memory pressure level.
        // note that this will reset the time to next decrement if the decrementer
        // is already running, which is the desired behaviour because we just got
        // a new low-mem notification.
        mPressureDecrementer.start();

        if (oldLevel == level) {
            // if we're not going to a higher level we probably don't
            // need to run another round of the same memory reductions
            // we did on the last memory pressure increase.
            return false;
        }

        // TODO hook in memory-reduction stuff for different levels here
        if (level >= MEMORY_PRESSURE_MEDIUM) {
            //Only send medium or higher events because that's all that is used right now
            if (GeckoThread.isRunning()) {
                dispatchMemoryPressure();
            }

            MemoryStorage.get().evictAll();
            ImageLoader.clearLruCache();
            LocalBroadcastManager.getInstance(mAppContext)
                    .sendBroadcast(new Intent(BrowserProvider.ACTION_SHRINK_MEMORY));
        }
        return true;
    }

    /**
     * Thread-safe due to mStoragePressure's volatility.
     */
    boolean isUnderStoragePressure() {
        return mStoragePressure;
    }

    private boolean decreaseMemoryPressure() {
        int newLevel;
        synchronized (this) {
            if (mMemoryPressure <= 0) {
                return false;
            }

            newLevel = --mMemoryPressure;
        }
        Log.d(LOGTAG, "Decreased memory pressure to " + newLevel);

        return true;
    }

    class PressureDecrementer implements Runnable {
        private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes

        private boolean mPosted;

        synchronized void start() {
            if (mPosted) {
                // cancel the old one before scheduling a new one
                ThreadUtils.getBackgroundHandler().removeCallbacks(this);
            }
            ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
            mPosted = true;
        }

        @Override
        public synchronized void run() {
            if (!decreaseMemoryPressure()) {
                // done decrementing, bail out
                mPosted = false;
                return;
            }

            // need to keep decrementing
            ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
        }
    }

    private static class StorageReducer implements Runnable {
        private final Context mContext;
        private final BrowserDB mDB;

        public StorageReducer(final Context context) {
            this.mContext = context;
            // Since this may be called while Fennec is in the background, we don't want to risk accidentally
            // using the wrong context. If the profile we get is a guest profile, use the default profile instead.
            GeckoProfile profile = GeckoProfile.get(mContext);
            if (profile.inGuestMode()) {
                // If it was the guest profile, switch to the default one.
                profile = GeckoProfile.get(mContext, GeckoProfile.DEFAULT_PROFILE);
            }

            mDB = BrowserDB.from(profile);
        }

        @Override
        public void run() {
            // this might get run right on startup, if so wait 10 seconds and try again
            if (!GeckoThread.isRunning()) {
                ThreadUtils.getBackgroundHandler().postDelayed(this, 10000);
                return;
            }

            if (!MemoryMonitor.getInstance().isUnderStoragePressure()) {
                // Pressure is off, so we can abort.
                return;
            }

            final ContentResolver cr = mContext.getContentResolver();
            mDB.expireHistory(cr, BrowserContract.ExpirePriority.AGGRESSIVE);
            mDB.removeThumbnails(cr);

            // TODO: drop or shrink disk caches
        }
    }

    // Needed to implement the ComponentCallbacks2 interface - we're only really interested in the
    // memory-related calls, though.
    @Override
    public void onConfigurationChanged(Configuration newConfig) { }
}
