package org.ebookdroid.common.bitmaps;

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

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

public class ByteBufferManager {

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

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

    private static final int GENERATION_THRESHOLD = 10;

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

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

    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();

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

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

                    if (ref.size >= 4 * width * height) {
                        if (ref.used.compareAndSet(false, true)) {
                            it.remove();

                            ref.pixels.rewind();
                            ref.gen = generation.get();
                            ref.width = width;
                            ref.height = height;
                            used.append(ref.id, ref);

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

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

            final ByteBufferBitmap ref = new ByteBufferBitmap(width, height);
            used.put(ref.id, ref);

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

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

            shrinkPool(BITMAP_MEMORY_LIMIT);

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

    public static ByteBufferBitmap[] getParts(final int partSize, final int rows, final int columns) {
        lock.lock();
        try {
            if (LCTX.isDebugEnabled()) {
                if (memoryUsed.get() + memoryPooled.get() == 0) {
                    LCTX.d("!!! Bitmap pool size: " + (BITMAP_MEMORY_LIMIT / 1024) + "KB");
                }
            }

            int filled = 0;
            final int length = rows * columns;
            final ByteBufferBitmap[] arr = new ByteBufferBitmap[length];

            final int size = 4 * partSize * partSize;
            final TLIterator<ByteBufferBitmap> it = pool.iterator();
            try {
                while (filled < length && it.hasNext()) {
                    final ByteBufferBitmap ref = it.next();

                    if (ref.size == size) {
                        if (ref.used.compareAndSet(false, true)) {
                            it.remove();

                            ref.pixels.rewind();
                            ref.gen = generation.get();
                            ref.width = partSize;
                            ref.height = partSize;
                            used.append(ref.id, ref);

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

                            arr[filled++] = ref;
                        } else {
                            if (LCTX.isDebugEnabled()) {
                                LCTX.d("Attempt to re-use used bitmap: " + ref);
                            }
                        }
                    }
                }
            } finally {
                it.release();
            }

            final int reused = filled;
            final int additional = length - filled;
            if (additional > 0) {
                for (int i = 0; i < additional; i++) {
                    final ByteBufferBitmap ref = new ByteBufferBitmap(partSize, partSize);
                    arr[filled++] = ref;

                    used.append(ref.id, ref);
                    created.incrementAndGet();
                    memoryUsed.addAndGet(ref.size);
                }
            }
            if (LCTX.isDebugEnabled()) {
                LCTX.d("Parts created : " + (additional) + ", resused: " + reused + ". Totally created=" + created
                        + ", reused=" + reused + ", memoryUsed=" + used.size() + "/" + (memoryUsed.get() / 1024) + "KB"
                        + ", memoryInPool=" + pool.size() + "/" + (memoryPooled.get() / 1024) + "KB");
            }

            if (additional > 0) {
                shrinkPool(BITMAP_MEMORY_LIMIT);
            }
            return arr;
        } 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();
        }
    }

    @SuppressWarnings("unchecked")
    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 ByteBufferBitmap) {
                    releaseImpl((ByteBufferBitmap) ref);
                    count++;
                } else if (ref instanceof GLBitmaps) {
                    final GLBitmaps bmp = (GLBitmaps) ref;
                    final ByteBufferBitmap[] bitmaps = bmp.clear();
                    if (bitmaps != null) {
                        for (final ByteBufferBitmap b : bitmaps) {
                            releaseImpl(b);
                            count++;
                        }
                    }
                } else if (ref instanceof List) {
                    final List<GLBitmaps> list = (List<GLBitmaps>) ref;
                    for (final GLBitmaps bmp : list) {
                        final ByteBufferBitmap[] bitmaps = bmp.clear();
                        if (bitmaps != null) {
                            for (final ByteBufferBitmap b : bitmaps) {
                                releaseImpl(b);
                                count++;
                            }
                        }
                    }
                } else if (ref instanceof ByteBufferBitmap[]) {
                    final ByteBufferBitmap[] bitmaps = (ByteBufferBitmap[]) ref;
                    for (final ByteBufferBitmap bitmap : bitmaps) {
                        if (bitmap != null) {
                            releaseImpl(bitmap);
                            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 ByteBufferBitmap ref) {
        if (ref != null) {
            if (LCTX.isDebugEnabled()) {
                LCTX.d("Adding 1 ref to release queue");
            }
            releasing.add(ref);
        }
    }

    public static void release(final ByteBufferBitmap[] refs) {
        if (refs != null) {
            if (LCTX.isDebugEnabled()) {
                LCTX.d("Adding " + refs.length + " refs to release queue");
            }
            releasing.add(refs);
        }
    }

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

    public static void release(final List<GLBitmaps> bitmapsToRecycle) {
        if (LengthUtils.isNotEmpty(bitmapsToRecycle)) {
            if (LCTX.isDebugEnabled()) {
                LCTX.d("Adding  list of " + bitmapsToRecycle.size() + " bitmaps to release queue");
            }
            releasing.add(new ArrayList<GLBitmaps>(bitmapsToRecycle));
        }
    }

    static void releaseImpl(final ByteBufferBitmap 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<ByteBufferBitmap> it = pool.iterator();
        while (it.hasNext()) {
            final ByteBufferBitmap 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 ByteBufferBitmap 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");
            }
        }
    }
}
