/* -*- 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.gfx.BitmapUtils;
import org.mozilla.gecko.mozglue.DirectBufferAllocator;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.util.Log;
import android.util.TypedValue;

import java.nio.ByteBuffer;
import java.util.ArrayList;

/**
 * Helper class to generate thumbnails for tabs.
 * Internally, a queue of pending thumbnails is maintained in mPendingThumbnails.
 * The head of the queue is the thumbnail that is currently being processed; upon
 * completion of the current thumbnail the next one is automatically processed.
 * Changes to the thumbnail width are stashed in mPendingWidth and the change is
 * applied between thumbnail processing. This allows a single thumbnail buffer to
 * be used for all thumbnails.
 */
public final class ThumbnailHelper {
    private static final boolean DEBUG = false;
    private static final String LOGTAG = "GeckoThumbnailHelper";

    public static final float TABS_PANEL_THUMBNAIL_ASPECT_RATIO = 0.8333333f;
    public static final float TOP_SITES_THUMBNAIL_ASPECT_RATIO = 0.571428571f;  // this is a 4:7 ratio (as per UX decision)
    public static final float THUMBNAIL_ASPECT_RATIO;

    static {
      // As we only want to generate one thumbnail for each tab, we calculate the
      // largest aspect ratio required and create the thumbnail based off that.
      // Any views with a smaller aspect ratio will use a cropped version of the
      // same image.
      THUMBNAIL_ASPECT_RATIO = Math.max(TABS_PANEL_THUMBNAIL_ASPECT_RATIO, TOP_SITES_THUMBNAIL_ASPECT_RATIO);
    }

    public enum CachePolicy {
        STORE,
        NO_STORE
    }

    // static singleton stuff

    private static ThumbnailHelper sInstance;

    public static synchronized ThumbnailHelper getInstance() {
        if (sInstance == null) {
            sInstance = new ThumbnailHelper();
        }
        return sInstance;
    }

    // instance stuff

    private final ArrayList<Tab> mPendingThumbnails;    // synchronized access only
    private volatile int mPendingWidth;
    private int mWidth;
    private int mHeight;
    private ByteBuffer mBuffer;

    private ThumbnailHelper() {
        final Resources res = GeckoAppShell.getContext().getResources();

        mPendingThumbnails = new ArrayList<>();
        try {
            mPendingWidth = (int) res.getDimension(R.dimen.tab_thumbnail_width);
        } catch (Resources.NotFoundException nfe) {
        }
        mWidth = -1;
        mHeight = -1;
    }

    public void getAndProcessThumbnailFor(final int tabId, final BitmapUtils.BitmapLoader loader) {
        final Tab tab = Tabs.getInstance().getTab(tabId);
        if (tab != null) {
            getAndProcessThumbnailFor(tab, loader);
        }
    }

    public void getAndProcessThumbnailFor(final Tab tab, final BitmapUtils.BitmapLoader loader) {
        BitmapUtils.runOnBitmapFoundOnUiThread(loader, tab.getThumbnail());

        Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() {
                @Override
                public void onTabChanged(final Tab t, final Tabs.TabEvents msg, final String data) {
                    if (tab != t || msg != Tabs.TabEvents.THUMBNAIL) {
                        return;
                    }
                    Tabs.unregisterOnTabsChangedListener(this);
                    BitmapUtils.runOnBitmapFoundOnUiThread(loader, t.getThumbnail());
                }
            });
        getAndProcessThumbnailFor(tab);
    }

    public void getAndProcessThumbnailFor(Tab tab) {
        if (AboutPages.isAboutHome(tab.getURL()) || AboutPages.isAboutPrivateBrowsing(tab.getURL())) {
            tab.updateThumbnail(null, CachePolicy.NO_STORE);
            return;
        }

        synchronized (mPendingThumbnails) {
            if (mPendingThumbnails.lastIndexOf(tab) > 0) {
                // This tab is already in the queue, so don't add it again.
                // Note that if this tab is only at the *head* of the queue,
                // (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do
                // add it again because it may have already been thumbnailed
                // and now we need to do it again.
                return;
            }

            mPendingThumbnails.add(tab);
            if (mPendingThumbnails.size() > 1) {
                // Some thumbnail was already being processed, so wait
                // for that to be done.
                return;
            }

            requestThumbnailLocked(tab);
        }
    }

    public void setThumbnailWidth(int width) {
        // Check inverted for safety: Bug 803299 Comment 34.
        if (GeckoAppShell.getScreenDepth() == 24) {
            mPendingWidth = width;
        } else {
            // Bug 776906: on 16-bit screens we need to ensure an even width.
            mPendingWidth = (width + 1) & (~1);
        }
    }

    private void updateThumbnailSizeLocked() {
        // Apply any pending width updates.
        mWidth = mPendingWidth;
        mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO);

        int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
        int capacity = mWidth * mHeight * pixelSize;
        if (DEBUG) {
            Log.d(LOGTAG, "Using new thumbnail size: " + capacity +
                          " (width " + mWidth + " - height " + mHeight + ")");
        }
        if (mBuffer == null || mBuffer.capacity() != capacity) {
            if (mBuffer != null) {
                mBuffer = DirectBufferAllocator.free(mBuffer);
            }
            try {
                mBuffer = DirectBufferAllocator.allocate(capacity);
            } catch (IllegalArgumentException iae) {
                Log.w(LOGTAG, iae.toString());
            } catch (OutOfMemoryError oom) {
                Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity);
            }
            // If we hit an error above, mBuffer will be pointing to null, so we are in a sane state.
        }
    }

    private void requestThumbnailLocked(Tab tab) {
        updateThumbnailSizeLocked();

        if (mBuffer == null) {
            // Buffer allocation may have failed. In this case we can't send the
            // event requesting the screenshot which means we won't get back a response
            // and so our queue will grow unboundedly. Handle this scenario by clearing
            // the queue (no point trying more thumbnailing right now since we're likely
            // low on memory). We will try again normally on the next call to
            // getAndProcessThumbnailFor which will hopefully be when we have more free memory.
            mPendingThumbnails.clear();
            return;
        }

        if (DEBUG) {
            Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight);
        }
        requestThumbnailLocked(mBuffer, tab, tab.getId(), mWidth, mHeight);
    }

    @WrapForJNI(stubName = "RequestThumbnail", dispatchTo = "proxy")
    private static native void requestThumbnailLocked(ByteBuffer data, Tab tab, int tabId,
                                                      int width, int height);

    /* This method is invoked by JNI once the thumbnail data is ready. */
    @WrapForJNI(calledFrom = "gecko")
    private static void notifyThumbnail(final ByteBuffer data, final Tab tab,
                                        final boolean success, final boolean shouldStore) {
        final ThumbnailHelper helper = ThumbnailHelper.getInstance();
        if (success) {
            helper.handleThumbnailData(
                    tab, data, shouldStore ? CachePolicy.STORE : CachePolicy.NO_STORE);
        }
        helper.processNextThumbnail();
    }

    private void processNextThumbnail() {
        synchronized (mPendingThumbnails) {
            if (mPendingThumbnails.isEmpty()) {
                return;
            }

            mPendingThumbnails.remove(0);

            if (!mPendingThumbnails.isEmpty()) {
                requestThumbnailLocked(mPendingThumbnails.get(0));
            }
        }
    }

    private void handleThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) {
        if (DEBUG) {
            Log.d(LOGTAG, "handleThumbnailData: " + data.capacity());
        }
        if (data != mBuffer) {
            // This should never happen, but log it and recover gracefully
            Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!");
        }

        processThumbnailData(tab, data, cachePolicy);
    }

    private void processThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) {
        Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight);
        data.position(0);
        b.copyPixelsFromBuffer(data);
        setTabThumbnail(tab, b, null, cachePolicy);
    }

    private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed, CachePolicy cachePolicy) {
        if (bitmap == null) {
            if (compressed == null) {
                Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!");
                return;
            }
            bitmap = BitmapUtils.decodeByteArray(compressed);
        }
        tab.updateThumbnail(bitmap, cachePolicy);
    }
}
