/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you 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 org.apache.logging.log4j.perf.jmh;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.perf.nogc.OpenHashStringMap;
import org.apache.logging.log4j.util.SortedArrayStringMap;
import org.apache.logging.log4j.util.TriConsumer;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

/**
 * Compares performance of SortedArrayStringMap vs. OpenHashMap vs. JDK HashMap.
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class SortedArrayVsHashMapBenchmark {

    // @Param({"1", "2", "5", "11", "23", "47", "95", "191", "383"})
    // @Param({"1", "5", "50", "500"})
    @Param({"5", "500"})
    public int count;

    // @Param({"5", "50"})
    @Param({"20"})
    public int length;

    private String[] keys;
    private static Object value = new Object();
    private HashMap<String, Object> map;
    private SortedArrayStringMap sortedStringArrayMap;
    private OpenHashStringMap<String, Object> openHashMapContextData;

    private HashMap<String, Object> populatedMap;
    private SortedArrayStringMap populatedSortedStringArrayMap;
    private OpenHashStringMap<String, Object> populatedOpenHashContextData;

    @Setup
    public void setup() {
        openHashMapContextData = new OpenHashStringMap<>();
        sortedStringArrayMap = new SortedArrayStringMap();
        map = new HashMap<>();

        keys = new String[count];
        final Random r = new Random();
        for (int j = 0; j < keys.length; j++) {
            final char[] str = new char[length];
            for (int i = 0; i < str.length; i++) {
                str[i] = (char) r.nextInt();
            }
            keys[j] = new String(str);
        }

        populatedMap = new HashMap<>();
        for (int i = 0; i < count; i++) {
            populatedMap.put(keys[i], value);
        }
        populatedSortedStringArrayMap = new SortedArrayStringMap();
        for (int i = 0; i < count; i++) {
            populatedSortedStringArrayMap.putValue(keys[i], value);
        }
        populatedOpenHashContextData = new OpenHashStringMap<>();
        for (int i = 0; i < count; i++) {
            populatedOpenHashContextData.putValue(keys[i], value);
        }
    }

    @Benchmark
    public SortedArrayStringMap putAllArrayContextData() {
        sortedStringArrayMap.clear();
        sortedStringArrayMap.putAll(populatedSortedStringArrayMap);
        return sortedStringArrayMap;
    }

    @Benchmark
    public OpenHashStringMap<String, Object> putAllHashContextData() {
        openHashMapContextData.clear();
        openHashMapContextData.putAll(populatedOpenHashContextData);
        return openHashMapContextData;
    }

    @Benchmark
    public Map putAllMap() {
        map.clear();
        map.putAll(populatedMap);
        return map;
    }

    @Benchmark
    public SortedArrayStringMap cloneArrayContextData() {
        return new SortedArrayStringMap(populatedSortedStringArrayMap);
    }

    @Benchmark
    public OpenHashStringMap<String, Object> cloneHashContextData() {
        return new OpenHashStringMap<>(populatedOpenHashContextData);
    }

    @Benchmark
    public Map cloneMap() {
        return new HashMap(populatedMap);
    }

    static TriConsumer<String, Object, int[]> COUNTER = (s, o, result) -> result[0] += s.hashCode() + o.hashCode();

    @Benchmark
    public int iterateArrayContextDataTriConsumer() {
        final int[] result = {0};

        populatedSortedStringArrayMap.forEach(COUNTER, result);
        return result[0];
    }

    @Benchmark
    public int iterateHashContextDataTriConsumer() {
        final int[] result = {0};

        populatedOpenHashContextData.forEach(COUNTER, result);
        return result[0];
    }

    @Benchmark
    public int iterateArrayContextDataBiConsumer() {
        final int[] result = {0};

        populatedSortedStringArrayMap.forEach((s, o) -> result[0] += s.hashCode() + o.hashCode());
        return result[0];
    }

    @Benchmark
    public int iterateHashContextDataBiConsumer() {
        final int[] result = {0};

        populatedOpenHashContextData.forEach((s, o) -> result[0] += s.hashCode() + o.hashCode());
        return result[0];
    }

    @Benchmark
    public int iterateMap() {
        final int[] result = {0};

        for (final Map.Entry<String, Object> entry : populatedMap.entrySet()) {
            result[0] += entry.getKey().hashCode() + entry.getValue().hashCode();
        }
        return result[0];
    }

    @Benchmark
    public Object getValueArrayContextData() {
        return populatedSortedStringArrayMap.getValue(keys[count - 1]);
    }

    @Benchmark
    public Object getValueHashContextData() {
        return populatedOpenHashContextData.getValue(keys[count - 1]);
    }

    @Benchmark
    public Object getValueMap() {
        return populatedMap.get(keys[count - 1]);
    }

    @Benchmark
    public int putArrayContextData() {
        populatedSortedStringArrayMap.putValue("someKey", "someValue");
        return populatedSortedStringArrayMap.size();
    }

    @Benchmark
    public int putHashContextData() {
        openHashMapContextData.put("someKey", "someValue");
        return openHashMapContextData.size();
    }

    @Benchmark
    public int putMap() {
        map.put("someKey", "someValue");
        return map.size();
    }
}
