package com.bumptech.glide.load.engine.prefill;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.google.common.collect.Range;

import android.graphics.Bitmap;

import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.engine.cache.MemoryCache;
import com.bumptech.glide.util.Util;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.util.ArrayList;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, emulateSdk = 18)
public class BitmapPreFillerTest {
    private static final int DEFAULT_BITMAP_WIDTH = 100;
    private static final int DEFAULT_BITMAP_HEIGHT = 50;

    private static final int BITMAPS_IN_POOL = 10;
    private static final int BITMAPS_IN_CACHE = 10;

    private final Bitmap.Config defaultBitmapConfig = PreFillType.DEFAULT_CONFIG;
    private final Bitmap defaultBitmap =
            Bitmap.createBitmap(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT, defaultBitmapConfig);
    private final int defaultBitmapSize = Util.getBitmapByteSize(defaultBitmap);
    private final int poolSize = BITMAPS_IN_CACHE * defaultBitmapSize;
    private final int cacheSize = BITMAPS_IN_POOL * defaultBitmapSize;

    private BitmapPool pool;
    private BitmapPreFiller bitmapPreFiller;
    private MemoryCache cache;

    @Before
    public void setUp() {
        pool = mock(BitmapPool.class);
        when(pool.getMaxSize()).thenReturn(poolSize);
        cache = mock(MemoryCache.class);
        when(cache.getMaxSize()).thenReturn(cacheSize);

        bitmapPreFiller = new BitmapPreFiller(cache, pool, DecodeFormat.DEFAULT);
    }

    @Test
    public void testAllocationOrderContainsEnoughSizesToFillPoolAndMemoryCache() {
        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(
                new PreFillType[] {
                        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                                .setConfig(defaultBitmapConfig)
                                .build()
                }
        );

        assertEquals(BITMAPS_IN_POOL + BITMAPS_IN_CACHE, allocationOrder.getSize());
    }

    @Test
    public void testAllocationOrderThatDoesNotFitExactlyIntoGivenSizeRoundsDown() {
        PreFillType[] sizes = new PreFillType[] {
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                        .setConfig(defaultBitmapConfig)
                        .build(),
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                        .setConfig(defaultBitmapConfig)
                        .build(),
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
                        .setConfig(defaultBitmapConfig)
                        .build(),
        };
        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(sizes);

        int byteSize = 0;
        while (!allocationOrder.isEmpty()) {
            PreFillType current = allocationOrder.remove();
            byteSize += Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig());
        }

        int expectedSize = 0;
        int maxSize = poolSize + cacheSize;
        for (PreFillType current : sizes) {
            int currentSize = Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig());
            expectedSize += currentSize * (maxSize / (3 * currentSize));
        }

        assertEquals(expectedSize, byteSize);
    }

    @Test
    public void testAllocationOrderDoesNotOverFillWithMultipleSizes() {
        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(
                new PreFillType[]{
                        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                                .setConfig(defaultBitmapConfig)
                                .build(),
                        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                                .setConfig(defaultBitmapConfig)
                                .build(),
                        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
                                .setConfig(defaultBitmapConfig)
                                .build()
                }
        );

        int byteSize = 0;
        while (!allocationOrder.isEmpty()) {
            PreFillType current = allocationOrder.remove();
            byteSize += Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig());
        }

        assertThat(byteSize).isIn(Range.atMost(poolSize + cacheSize));
    }

    @Test
    public void testAllocationOrderDoesNotOverFillWithMultipleSizesAndWeights() {
        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(
                new PreFillType[]{
                        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                                .setConfig(defaultBitmapConfig)
                                .setWeight(4)
                                .build(),
                        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                                .setConfig(defaultBitmapConfig)
                                .build(),
                        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 3)
                                .setConfig(defaultBitmapConfig)
                                .setWeight(3)
                                .build()
                }
        );

        int byteSize = 0;
        while (!allocationOrder.isEmpty()) {
            PreFillType current = allocationOrder.remove();
            byteSize += Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig());
        }

        assertThat(byteSize).isIn(Range.atMost(poolSize + cacheSize));
    }

    @Test
    public void testAllocationOrderContainsSingleSizeIfSingleSizeIsProvided() {
        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(
                new PreFillType[] {
                    new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                            .setConfig(defaultBitmapConfig)
                            .build()
                }
        );

        while (!allocationOrder.isEmpty()) {
            PreFillType size = allocationOrder.remove();
            assertEquals(DEFAULT_BITMAP_WIDTH, size.getWidth());
            assertEquals(DEFAULT_BITMAP_HEIGHT, size.getHeight());
            assertEquals(defaultBitmapConfig, size.getConfig());
        }
    }

    @Test
    public void testAllocationOrderSplitsEvenlyBetweenEqualSizesWithEqualWeights() {
        PreFillType smallWidth = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                .setConfig(defaultBitmapConfig)
                .build();
        PreFillType smallHeight =
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
                        .setConfig(defaultBitmapConfig)
                        .build();
        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(
                new PreFillType[] { smallWidth, smallHeight, }
        );

        int numSmallWidth = 0, numSmallHeight = 0;
        while (!allocationOrder.isEmpty()) {
            PreFillType current = allocationOrder.remove();
            if (smallWidth.equals(current)) {
                numSmallWidth++;
            } else if (smallHeight.equals(current)) {
                numSmallHeight++;
            } else {
                fail("Unexpected size, size: " + current);
            }
        }

        assertEquals(numSmallWidth, numSmallHeight);
    }

    @Test
    public void testAllocationOrderSplitsByteSizeEvenlyBetweenUnEqualSizesWithEqualWeights() {
        PreFillType smallWidth =
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                        .setConfig(defaultBitmapConfig)
                        .build();
        PreFillType normal =
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                        .setConfig(defaultBitmapConfig)
                        .build();
        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(
                new PreFillType[] { smallWidth, normal }
        );

        int numSmallWidth = 0, numNormal = 0;
        while (!allocationOrder.isEmpty()) {
            PreFillType current = allocationOrder.remove();
            if (smallWidth.equals(current)) {
                numSmallWidth++;
            } else if (normal.equals(current)) {
                numNormal++;
            } else {
                fail("Unexpected size, size: " + current);
            }
        }

        assertEquals(2 * numNormal, numSmallWidth);
    }

    @Test
    public void testAllocationOrderSplitsByteSizeUnevenlyBetweenEqualSizesWithUnequalWeights() {
        PreFillType doubleWeight =
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                        .setConfig(defaultBitmapConfig)
                        .setWeight(2)
                        .build();
        PreFillType normal = new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
                .setConfig(defaultBitmapConfig)
                .build();
        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(
                new PreFillType[] { doubleWeight, normal }
        );

        int numDoubleWeight = 0, numNormal = 0;
        while (!allocationOrder.isEmpty()) {
            PreFillType current = allocationOrder.remove();
            if (doubleWeight.equals(current)) {
                numDoubleWeight++;
            } else if (normal.equals(current)) {
                numNormal++;
            } else {
                fail("Unexpected size, size: " + current);
            }
        }

        assertEquals(2 * numNormal, numDoubleWeight);
    }

    @Test
    public void testAllocationOrderRoundRobinsDifferentSizes() {
        when(pool.getMaxSize()).thenReturn(defaultBitmapSize);
        when(cache.getMaxSize()).thenReturn(defaultBitmapSize);
        PreFillType smallWidth =
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                        .setConfig(defaultBitmapConfig)
                        .build();
        PreFillType smallHeight =
                new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
                        .setConfig(defaultBitmapConfig)
                        .build();

        PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(
                new PreFillType[] { smallWidth, smallHeight, }
        );

        List<PreFillType> attributes = new ArrayList<PreFillType>();
        while (!allocationOrder.isEmpty()) {
            attributes.add(allocationOrder.remove());
        }

        // Either width, height, width, height or height, width, height, width.
        try {
            assertThat(attributes).containsExactly(smallWidth, smallHeight, smallWidth, smallHeight).inOrder();
        } catch (AssertionError e) {
            assertThat(attributes).containsExactly(smallHeight, smallWidth, smallHeight, smallWidth).inOrder();
        }
    }

    @Test
    public void testSetsConfigOnBuildersToDefaultIfNotSet() {
        PreFillType.Builder builder = mock(PreFillType.Builder.class);
        when(builder.build()).thenReturn(new PreFillType.Builder(100).setConfig(Bitmap.Config.RGB_565).build());

        bitmapPreFiller.preFill(builder);

        InOrder order = inOrder(builder);
        order.verify(builder).setConfig(Bitmap.Config.RGB_565);
        order.verify(builder).build();
    }

    @Test
    public void testDoesNotSetConfigOnBuildersIfConfigIsAlreadySet() {
        PreFillType.Builder builder = mock(PreFillType.Builder.class);

        when(builder.getConfig()).thenReturn(Bitmap.Config.ARGB_4444);
        when(builder.build()).thenReturn(new PreFillType.Builder(100).setConfig(Bitmap.Config.ARGB_4444).build());
        bitmapPreFiller.preFill(builder);

        verify(builder, never()).setConfig(any(Bitmap.Config.class));
    }
}