/*
 * Copyright 2012-2018 Chronicle Map Contributors
 *
 * 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 net.openhft.chronicle.map;

import net.openhft.chronicle.threads.NamedThreadFactory;
import org.junit.Ignore;
import org.junit.Test;

import java.beans.XMLEncoder;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;

import static org.junit.Assert.*;

/**
 * Created by peter.lawrey on 06/12/14.
 */
public class LargeEntriesTest {

    @Test
    public void testLargeStrings() throws ExecutionException, InterruptedException, IOException {
        final int ENTRIES = 250;
        final int ENTRY_SIZE = 100 * 1024;

        File file = File.createTempFile("largeEntries" + System.currentTimeMillis(), ".deleteme");
        file.deleteOnExit();
        try (final ChronicleMap<String, String> map = ChronicleMapBuilder
                .of(String.class, String.class)
//                .valueReaderAndDataAccess(, SnappyStringMarshaller.INSTANCE, )
                .actualSegments(1) // to force an error.
                .entries(ENTRIES)
                .averageKeySize(10)
                .averageValueSize(ENTRY_SIZE)
                .putReturnsNull(true)
                .createPersistedTo(file)) {
            warmUpCompression(ENTRY_SIZE);
            int threads = 4; //Runtime.getRuntime().availableProcessors();
            ExecutorService es = Executors.newFixedThreadPool(threads, new NamedThreadFactory("test"));
            final int block = ENTRIES / threads;
            for (int i = 0; i < 3; i++) {
                long start = System.currentTimeMillis();
                List<Future<?>> futureList = new ArrayList<>();
                for (int t = 0; t < threads; t++) {
                    final int finalT = t;
                    futureList.add(es.submit(new Runnable() {
                        @Override
                        public void run() {
                            exerciseLargeStrings(map, finalT * block, finalT * block + block,
                                    ENTRY_SIZE);
                        }
                    }));
                }
                for (Future<?> future : futureList) {
                    future.get();
                }
                long time = System.currentTimeMillis() - start;
                long operations = 3;
                System.out.printf("Put/Get rate was %.1f MB/s%n",
                        operations * ENTRIES * ENTRY_SIZE / 1e6 / (time / 1e3));
            }
            es.shutdown();
            es.awaitTermination(1, TimeUnit.MINUTES);
            assertTrue(es.isTerminated());
        }
        file.delete();
    }

    private void warmUpCompression(int entrySize) {
//        String value = generateValue(entrySize);
//        DirectBytes bytes = DirectStore.allocate(entrySize / 6).bytes();
//        for (int i = 0; i < 5; i++) {
//             warmup to compression.
//            bytes.clear();
//            SnappyStringMarshaller.INSTANCE.write(bytes, value);
//            bytes.flip();
//            SnappyStringMarshaller.INSTANCE.read(bytes);
//        }
//        bytes.release();
    }

    @Test
    @Ignore("Performance Test")
    public void testLargeStringsPerf() throws ExecutionException, InterruptedException, IOException {
        doLargeEntryPerf(10000, 100 * 1024);
        doLargeEntryPerf(1000000, 1024);
        doLargeEntryPerf(100000, 10 * 1024);
        doLargeEntryPerf(10000, 100 * 1024);
        doLargeEntryPerf(3000, 1024 * 1024);
    }

    private void doLargeEntryPerf(int ENTRIES, final int ENTRY_SIZE) throws IOException, InterruptedException, ExecutionException {
        System.out.printf("Testing %,d entries of %,d KB%n", ENTRIES, ENTRY_SIZE / 1024);
        File file = File.createTempFile("largeEntries", ".deleteme");
        file.deleteOnExit();
        final ChronicleMap<String, String> map = ChronicleMapBuilder
                .of(String.class, String.class)
//                .valueReaderAndDataAccess(, SnappyStringMarshaller.INSTANCE, )
                .entries(ENTRIES)
                .averageKeySize(10)
                .averageValueSize(ENTRY_SIZE)
                .putReturnsNull(true)
                .createPersistedTo(file);
        {
//            warmUpCompression(ENTRY_SIZE);
            int threads = Runtime.getRuntime().availableProcessors();
            ExecutorService es = Executors.newFixedThreadPool(threads,
                    new NamedThreadFactory("test"));
            final int block = ENTRIES / threads;
            for (int i = 0; i < 3; i++) {
                long start = System.currentTimeMillis();
                List<Future<?>> futureList = new ArrayList<>();
                for (int t = 0; t < threads; t++) {
                    final int finalT = t;
                    futureList.add(es.submit(new Runnable() {
                        @Override
                        public void run() {
                            exerciseLargeStrings(map, finalT * block, finalT * block + block,
                                    ENTRY_SIZE);
                        }
                    }));
                }
                for (Future<?> future : futureList) {
                    future.get();
                }
                long time = System.currentTimeMillis() - start;
                long operations = 3;
                System.out.printf("Put/Get rate was %.1f MB/s%n", operations * ENTRIES * ENTRY_SIZE / 1e6 / (time / 1e3));
            }
            es.shutdown();
            if (es.isTerminated())
                map.close();
        }
    }

    void exerciseLargeStrings(ChronicleMap<String, String> map, int start, int finish, int entrySize) {
/*
        final Thread thisThread = Thread.currentThread();
        Thread monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                StringBuilder sb = new StringBuilder();
                sb.append(thisThread).append("\n");
                try {
                    while (!Thread.currentThread().isInterrupted()) {
                        Jvm.pause(50);
                        StackTraceElement[] stackTrace = thisThread.getStackTrace();
                        for (int i = 0; i < 4 && i < stackTrace.length; i++)
                            sb.append("\tat ").append(stackTrace[i]).append("\n");
                        sb.append("\n");
                    }
                } catch (InterruptedException e) {
                }
                System.out.println(sb);
            }
        });
        monitor.start();
*/
        String value = generateValue(entrySize);

        for (int i = start; i < finish; i++) {
            String key = "key-" + i;
            String object;

            map.put(key, value);
            object = map.get(key);
            assertTrue(key, map.containsKey(key));

            assertNotNull(key, object);
            assertEquals(key, entrySize, object.length());
        }
//        monitor.interrupt();

        for (int i = start; i < finish; i++) {
//            System.out.println(i);
            String key = "key-" + i;

            String object = map.get(key);

            assertNotNull(key, object);
            assertEquals(key, entrySize, object.length());
        }
    }

    private String generateValue(int entrySize) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        XMLEncoder xml = new XMLEncoder(baos);
        Map<String, String> map2 = new HashMap<>();
        Random rand = new Random(1);
        for (int i = 0; i < entrySize / 80; i++)
            map2.put("key-" + i, "value-" + rand.nextInt(1000));
        xml.writeObject(map2);
        xml.close();
        return baos.toString().substring(0, entrySize);
    }
}
