package org.ebookdroid.common.bitmaps;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.SparseArray;

import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;

import org.emdev.BaseDroidApp;
import org.emdev.common.log.LogContext;
import org.emdev.common.log.LogManager;
import org.emdev.utils.collections.ArrayDeque;
import org.emdev.utils.collections.SparseArrayEx;
import org.emdev.utils.collections.TLIterator;

public class BitmapManager {

    static final LogContext LCTX = LogManager.root().lctx("BitmapManager", false);

    private final static long BITMAP_MEMORY_LIMIT = Runtime.getRuntime().maxMemory() / 2;

    private static final int GENERATION_THRESHOLD = 10;

    private static SparseArrayEx<AbstractBitmapRef> used = new SparseArrayEx<AbstractBitmapRef>();

    private static ArrayDeque<AbstractBitmapRef> pool = new ArrayDeque<AbstractBitmapRef>();

    private static SparseArray<Bitmap> resources = new SparseArray<Bitmap>();

    private static Queue<Object> releasing = new ConcurrentLinkedQueue<Object>();

    private static final AtomicLong created = new AtomicLong();
    private static final AtomicLong reused = new AtomicLong();

    private static final AtomicLong memoryUsed = new AtomicLong();
    private static final AtomicLong memoryPooled = new AtomicLong();

    private static AtomicLong generation = new AtomicLong();

    private static ReentrantLock lock = new ReentrantLock();

    static int partSize = 1 << 7;

    public static Bitmap getResource(final int resourceId) {
        synchronized (resources) {
            Bitmap bitmap = resources.get(resourceId);
            if (bitmap == null || bitmap.isRecycled()) {
                final Resources resources = BaseDroidApp.context.getResources();
                bitmap = BitmapFactory.decodeResource(resources, resourceId);
            }
            return bitmap;
        }
    }

    public static IBitmapRef addBitmap(final String name, final Bitmap bitmap) {
        lock.lock();
        try {
            final BitmapRef ref = new BitmapRef(bitmap, generation.get());
            used.append(ref.id, ref);

            created.incrementAndGet();
            memoryUsed.addAndGet(ref.size);

            if (LCTX.isDebugEnabled()) {
                LCTX.d("Added bitmap: [" + ref.id + ", " + name + ", " + ref.width + ", " + ref.height + "], created="
                        + created + ", reused=" + reused + ", memoryUsed=" + used.size() + "/"
                        + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/"
                        + (memoryPooled.get() / 1024) + "KB");
            }

            ref.name = name;
            return ref;
        } finally {
            lock.unlock();
        }
    }

    public static IBitmapRef getBitmap(final String name, final int width, final int height, final Bitmap.Config config) {
        lock.lock();
        try {
            if (LCTX.isDebugEnabled()) {
                if (memoryUsed.get() + memoryPooled.get() == 0) {
                    LCTX.d("!!! Bitmap pool size: " + (BITMAP_MEMORY_LIMIT / 1024) + "KB");
                }
            }

            final TLIterator<AbstractBitmapRef> it = pool.iterator();
            try {
                while (it.hasNext()) {
                    final AbstractBitmapRef ref = it.next();

                    if (!ref.isRecycled() && ref.config == config && ref.width == width && ref.height >= height) {
                        if (ref.used.compareAndSet(false, true)) {
                            it.remove();

                            ref.gen = generation.get();
                            used.append(ref.id, ref);

                            reused.incrementAndGet();
                            memoryPooled.addAndGet(-ref.size);
                            memoryUsed.addAndGet(ref.size);

                            if (LCTX.isDebugEnabled()) {
                                LCTX.d("Reuse bitmap: [" + ref.id + ", " + ref.name + " => " + name + ", " + width
                                        + ", " + height + "], created=" + created + ", reused=" + reused
                                        + ", memoryUsed=" + used.size() + "/" + (memoryUsed.get() / 1024) + "KB"
                                        + ", memoryInPool=" + pool.size() + "/" + (memoryPooled.get() / 1024) + "KB");
                            }
                            ref.name = name;
                            return ref;
                        } else {
                            if (LCTX.isDebugEnabled()) {
                                LCTX.d("Attempt to re-use used bitmap: " + ref);
                            }
                        }
                    }
                }
            } finally {
                it.release();
            }

            final BitmapRef ref = new BitmapRef(Bitmap.createBitmap(width, height, config), generation.get());
            used.put(ref.id, ref);

            created.incrementAndGet();
            memoryUsed.addAndGet(ref.size);

            if (LCTX.isDebugEnabled()) {
                LCTX.d("Create bitmap: [" + ref.id + ", " + name + ", " + width + ", " + height + "], created="
                        + created + ", reused=" + reused + ", memoryUsed=" + used.size() + "/"
                        + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/"
                        + (memoryPooled.get() / 1024) + "KB");
            }

            shrinkPool(BITMAP_MEMORY_LIMIT);

            ref.name = name;
            return ref;
        } finally {
            lock.unlock();
        }
    }

    public static void clear(final String msg) {
        lock.lock();
        try {
            generation.addAndGet(GENERATION_THRESHOLD * 2);
            removeOldRefs();
            release();
            shrinkPool(0);
        } finally {
            lock.unlock();
        }
    }

    public static void release() {
        lock.lock();
        try {
            generation.incrementAndGet();
            removeOldRefs();

            int count = 0;
            final int queueBefore = LCTX.isDebugEnabled() ? releasing.size() : 0;
            while (!releasing.isEmpty()) {
                final Object ref = releasing.poll();
                if (ref instanceof AbstractBitmapRef) {
                    releaseImpl((AbstractBitmapRef) ref);
                    count++;
                } else {
                    LCTX.e("Unknown object in release queue: " + ref);
                }
            }

            shrinkPool(BITMAP_MEMORY_LIMIT);

            if (LCTX.isDebugEnabled()) {
                LCTX.d("Return " + count + " bitmap(s) to pool: " + "memoryUsed=" + used.size() + "/"
                        + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/"
                        + (memoryPooled.get() / 1024) + "KB" + ", releasing queue size " + queueBefore + " => 0");
            }
        } finally {
            lock.unlock();
        }
    }

    public static void release(final IBitmapRef ref) {
        if (ref != null) {
            if (LCTX.isDebugEnabled()) {
                LCTX.d("Adding 1 ref to release queue");
            }
            releasing.add(ref);
        }
    }

    static void releaseImpl(final AbstractBitmapRef ref) {
        assert ref != null;
        if (ref.used.compareAndSet(true, false)) {
            if (used.get(ref.id, null) == ref) {
                used.remove(ref.id);
                memoryUsed.addAndGet(-ref.size);
            } else {
                LCTX.e("The bitmap " + ref + " not found in used ones");
            }
        } else {
            if (LCTX.isDebugEnabled()) {
                LCTX.d("Attempt to release unused bitmap");
            }
        }

        pool.add(ref);
        memoryPooled.addAndGet(ref.size);
    }

    private static void removeOldRefs() {
        final long gen = generation.get();
        int recycled = 0;
        final Iterator<AbstractBitmapRef> it = pool.iterator();
        while (it.hasNext()) {
            final AbstractBitmapRef ref = it.next();
            if (gen - ref.gen > GENERATION_THRESHOLD) {
                it.remove();
                ref.recycle();
                recycled++;
                memoryPooled.addAndGet(-ref.size);
            }
        }
        if (recycled > 0) {
            if (LCTX.isDebugEnabled()) {
                LCTX.d("Recycled " + recycled + " pooled bitmap(s): " + "memoryUsed=" + used.size() + "/"
                        + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/"
                        + (memoryPooled.get() / 1024) + "KB");
            }
        }
    }

    private static void shrinkPool(final long limit) {
        int recycled = 0;
        while (memoryPooled.get() + memoryUsed.get() > limit && !pool.isEmpty()) {
            final AbstractBitmapRef ref = pool.poll();
            if (ref != null) {
                ref.recycle();
                memoryPooled.addAndGet(-ref.size);
                recycled++;
            }
        }

        if (recycled > 0) {
            if (LCTX.isDebugEnabled()) {
                LCTX.d("Recycled " + recycled + " pooled bitmap(s): " + "memoryUsed=" + used.size() + "/"
                        + (memoryUsed.get() / 1024) + "KB" + ", memoryInPool=" + pool.size() + "/"
                        + (memoryPooled.get() / 1024) + "KB");
            }
        }
    }

    public static int getBitmapBufferSize(final int width, final int height, final Bitmap.Config config) {
        return getPixelSizeInBytes(config) * width * height;
    }

    public static int getPartSize() {
        return partSize;
    }

    public static void setPartSize(final int partSize) {
        BitmapManager.partSize = partSize;
    }

    public static int getPixelSizeInBytes(final Bitmap.Config config) {
        switch (config) {
            case ALPHA_8:
                return 1;
            case ARGB_4444:
            case RGB_565:
                return 2;
            case ARGB_8888:
            default:
                return 4;
        }
    }
}
