/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.media.filterfw;

import androidx.media.filterfw.BackingStore.Backing;

import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;

/**
 * The FrameManager tracks, caches, allocates and deallocates frame data.
 * All Frame instances are managed by a FrameManager, and belong to exactly one of these. Frames
 * cannot be shared across FrameManager instances, however multiple MffContexts may use the same
 * FrameManager.
 *
 * Additionally, frame managers allow attaching Frames under a specified key. This allows decoupling
 * filter-graphs by instructing one node to attach a frame under a specific key, and another to
 * fetch the frame under the same key.
 */
public class FrameManager {

    /** The default max cache size is set to 12 MB */
    public final static int DEFAULT_MAX_CACHE_SIZE = 12 * 1024 * 1024;

    /** Frame caching policy: No caching */
    public final static int FRAME_CACHE_NONE = 0;
    /** Frame caching policy: Drop least recently used frame buffers */
    public final static int FRAME_CACHE_LRU = 1;
    /** Frame caching policy: Drop least frequently used frame buffers */
    public final static int FRAME_CACHE_LFU = 2;

    /** Slot Flag: No flags set */
    public final static int SLOT_FLAGS_NONE = 0x00;
    /** Slot Flag: Sticky flag set: Frame will remain in slot after fetch. */
    public final static int SLOT_FLAG_STICKY = 0x01;

    private GraphRunner mRunner;
    private Set<Backing> mBackings = new HashSet<Backing>();
    private BackingCache mCache;

    private Map<String, FrameSlot> mFrameSlots = new HashMap<String, FrameSlot>();

    static class FrameSlot {
        private FrameType mType;
        private int mFlags;
        private Frame mFrame = null;

        public FrameSlot(FrameType type, int flags) {
            mType = type;
            mFlags = flags;
        }

        public FrameType getType() {
            return mType;
        }

        public boolean hasFrame() {
            return mFrame != null;
        }

        public void releaseFrame() {
            if (mFrame != null) {
                mFrame.release();
                mFrame = null;
            }
        }

        // TODO: Type check
        public void assignFrame(Frame frame) {
            Frame oldFrame = mFrame;
            mFrame = frame.retain();
            if (oldFrame != null) {
                oldFrame.release();
            }
        }

        public Frame getFrame() {
            Frame result = mFrame.retain();
            if ((mFlags & SLOT_FLAG_STICKY) == 0) {
                releaseFrame();
            }
            return result;
        }

        public void markWritable() {
            if (mFrame != null) {
                mFrame.setReadOnly(false);
            }
        }
    }

    private static abstract class BackingCache {

        protected int mCacheMaxSize = DEFAULT_MAX_CACHE_SIZE;

        public abstract Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize);

        public abstract boolean cacheBacking(Backing backing);

        public abstract void clear();

        public abstract int getSizeLeft();

        public void setSize(int size) {
            mCacheMaxSize = size;
        }

        public int getSize() {
            return mCacheMaxSize;
        }
    }

    private static class BackingCacheNone extends BackingCache {

        @Override
        public Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) {
            return null;
        }

        @Override
        public boolean cacheBacking(Backing backing) {
            return false;
        }

        @Override
        public void clear() {
        }

        @Override
        public int getSize() {
            return 0;
        }

        @Override
        public int getSizeLeft() {
            return 0;
        }
    }

    private static abstract class PriorityBackingCache extends BackingCache {
        private int mSize = 0;
        private PriorityQueue<Backing> mQueue;

        public PriorityBackingCache() {
            mQueue = new PriorityQueue<Backing>(4, new Comparator<Backing>() {
                @Override
                public int compare(Backing left, Backing right) {
                    return left.cachePriority - right.cachePriority;
                }
            });
        }

        @Override
        public Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) {
            for (Backing backing : mQueue) {
                int backingAccess = (mode == Frame.MODE_WRITE)
                    ? backing.writeAccess()
                    : backing.readAccess();
                if ((backingAccess & access) == access
                    && dimensionsCompatible(backing.getDimensions(), dimensions)
                    && (elemSize == backing.getElementSize())) {
                    mQueue.remove(backing);
                    mSize -= backing.getSize();
                    onFetchBacking(backing);
                    return backing;
                }
            }
            //Log.w("FrameManager", "Could not find backing for dimensions " + Arrays.toString(dimensions));
            return null;
        }

        @Override
        public boolean cacheBacking(Backing backing) {
            if (reserve(backing.getSize())) {
                onCacheBacking(backing);
                mQueue.add(backing);
                return true;
            }
            return false;
        }

        @Override
        public void clear() {
            mQueue.clear();
            mSize = 0;
        }

        @Override
        public int getSizeLeft() {
            return mCacheMaxSize - mSize;
        }

        protected abstract void onCacheBacking(Backing backing);

        protected abstract void onFetchBacking(Backing backing);

        private boolean reserve(int size) {
            //Log.i("FM", "Reserving " + size + " bytes (max: " + mCacheMaxSize + " bytes).");
            //Log.i("FM", "Current size " + mSize);
            if (size > mCacheMaxSize) {
                return false;
            }
            mSize += size;
            while (mSize > mCacheMaxSize) {
                Backing dropped = mQueue.poll();
                mSize -= dropped.getSize();
                //Log.i("FM", "Dropping  " + dropped + " with priority "
                //    + dropped.cachePriority + ". New size: " + mSize + "!");
                dropped.destroy();
            }
            return true;
        }


    }

    private static class BackingCacheLru extends PriorityBackingCache {
        private int mTimestamp = 0;

        @Override
        protected void onCacheBacking(Backing backing) {
            backing.cachePriority = 0;
        }

        @Override
        protected void onFetchBacking(Backing backing) {
            ++mTimestamp;
            backing.cachePriority = mTimestamp;
        }
    }

    private static class BackingCacheLfu extends PriorityBackingCache {
        @Override
        protected void onCacheBacking(Backing backing) {
            backing.cachePriority = 0;
        }

        @Override
        protected void onFetchBacking(Backing backing) {
            ++backing.cachePriority;
        }
    }

    public static FrameManager current() {
        GraphRunner runner = GraphRunner.current();
        return runner != null ? runner.getFrameManager() : null;
    }

    /**
     * Returns the context that the FrameManager is bound to.
     *
     * @return the MffContext instance that the FrameManager is bound to.
     */
    public MffContext getContext() {
        return mRunner.getContext();
    }

    /**
     * Returns the GraphRunner that the FrameManager is bound to.
     *
     * @return the GraphRunner instance that the FrameManager is bound to.
     */
    public GraphRunner getRunner() {
        return mRunner;
    }

    /**
     * Sets the size of the cache.
     *
     * Resizes the cache to the specified size in bytes.
     *
     * @param bytes the new size in bytes.
     */
    public void setCacheSize(int bytes) {
        mCache.setSize(bytes);
    }

    /**
     * Returns the size of the cache.
     *
     * @return the size of the cache in bytes.
     */
    public int getCacheSize() {
        return mCache.getSize();
    }

    /**
     * Imports a frame from another FrameManager.
     *
     * This will return a frame with the contents of the given frame for use in this FrameManager.
     * Note, that there is a substantial cost involved in moving a Frame from one FrameManager to
     * another. This may be called from any thread. After the frame has been imported, it may be
     * used in the runner that uses this FrameManager. As the new frame may share data with the
     * provided frame, that frame must be read-only.
     *
     * @param frame The frame to import
     */
    public Frame importFrame(Frame frame) {
        if (!frame.isReadOnly()) {
            throw new IllegalArgumentException("Frame " + frame + " must be read-only to import "
                    + "into another FrameManager!");
        }
        return frame.makeCpuCopy(this);
    }

    /**
     * Adds a new frame slot to the frame manager.
     * Filters can reference frame slots to pass frames between graphs or runs. If the name
     * specified here is already taken the frame slot is overwritten. You can only
     * modify frame-slots while no graph of the frame manager is running.
     *
     * @param name The name of the slot.
     * @param type The type of Frame that will be assigned to this slot.
     * @param flags A mask of {@code SLOT} flags.
     */
    public void addFrameSlot(String name, FrameType type, int flags) {
        assertNotRunning();
        FrameSlot oldSlot = mFrameSlots.get(name);
        if (oldSlot != null) {
            removeFrameSlot(name);
        }
        FrameSlot slot = new FrameSlot(type, flags);
        mFrameSlots.put(name, slot);
    }

    /**
     * Removes a frame slot from the frame manager.
     * Any frame within the slot is released. You can only modify frame-slots while no graph
     * of the frame manager is running.
     *
     * @param name The name of the slot
     * @throws IllegalArgumentException if no such slot exists.
     */
    public void removeFrameSlot(String name) {
        assertNotRunning();
        FrameSlot slot = getSlot(name);
        slot.releaseFrame();
        mFrameSlots.remove(slot);
    }

    /**
     * TODO: Document!
     */
    public void storeFrame(Frame frame, String slotName) {
        assertInGraphRun();
        getSlot(slotName).assignFrame(frame);
    }

    /**
     * TODO: Document!
     */
    public Frame fetchFrame(String slotName) {
        assertInGraphRun();
        return getSlot(slotName).getFrame();
    }

    /**
     * Clears the Frame cache.
     */
    public void clearCache() {
        mCache.clear();
    }

    /**
     * Create a new FrameManager instance.
     *
     * Creates a new FrameManager instance in the specified context and employing a cache with the
     * specified cache type (see the cache type constants defined by the FrameManager class).
     *
     * @param runner the GraphRunner to bind the FrameManager to.
     * @param cacheType the type of cache to use.
     */
    FrameManager(GraphRunner runner, int cacheType) {
        mRunner = runner;
        switch (cacheType) {
            case FRAME_CACHE_NONE:
                mCache = new BackingCacheNone();
                break;
            case FRAME_CACHE_LRU:
                mCache = new BackingCacheLru();
                break;
            case FRAME_CACHE_LFU:
                mCache = new BackingCacheLfu();
                break;
            default:
                throw new IllegalArgumentException("Unknown cache-type " + cacheType + "!");
        }
    }

    Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) {
        return mCache.fetchBacking(mode, access, dimensions, elemSize);
    }

    void onBackingCreated(Backing backing) {
        if (backing != null) {
            mBackings.add(backing);
            // Log.i("FrameManager", "RM: Now have " + mBackings.size() + " backings");
        }
    }

    void onBackingAvailable(Backing backing) {
        if (!backing.shouldCache() || !mCache.cacheBacking(backing)) {
            backing.destroy();
            mBackings.remove(backing);
            //Log.i("FrameManager", "RM: Now have " + mBackings.size() + " backings (" + mCache.getSizeLeft() + ")");
        }
    }

    /**
     * Destroying all references makes any Frames that contain them invalid.
     */
    void destroyBackings() {
        for (Backing backing : mBackings) {
            backing.destroy();
        }
        mBackings.clear();
        mCache.clear();
    }

    FrameSlot getSlot(String name) {
        FrameSlot slot = mFrameSlots.get(name);
        if (slot == null) {
            throw new IllegalArgumentException("Unknown frame slot '" + name + "'!");
        }
        return slot;
    }

    void onBeginRun() {
        for (FrameSlot slot : mFrameSlots.values()) {
            slot.markWritable();
        }
    }

    // Internals ///////////////////////////////////////////////////////////////////////////////////
    private static boolean dimensionsCompatible(int[] dimA, int[] dimB) {
        return dimA == null || dimB == null || Arrays.equals(dimA, dimB);
    }

    private void assertNotRunning() {
        if (mRunner.isRunning()) {
            throw new IllegalStateException("Attempting to modify FrameManager while graph is "
                + "running!");
        }
    }

    private void assertInGraphRun() {
        if (!mRunner.isRunning() || GraphRunner.current() != mRunner) {
            throw new IllegalStateException("Attempting to access FrameManager Frame data "
                + "outside of graph run-loop!");
        }
    }

}

