/*
 * 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 accord.burn.random;

import accord.utils.Gen;
import accord.utils.Gens;
import accord.utils.RandomSource;
import org.agrona.collections.Long2LongHashMap;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.stream.LongStream;

import static accord.utils.Property.qt;

class FrequentLargeRangeTest
{
    enum Type
    {
        IntRange,
        RandomWalkRange
    }
    
    private static final class TestCase
    {
        private final int minSmall, maxSmall, minLarge, maxLarge, largeRatio;
        private final Type type;

        private TestCase(int minSmall, int maxSmall, int minLarge, int maxLarge, int largeRatio, Type type)
        {
            this.minSmall = minSmall;
            this.maxSmall = maxSmall;
            this.minLarge = minLarge;
            this.maxLarge = maxLarge;
            this.largeRatio = largeRatio;
            this.type = type;
        }

        Gen.LongGen min(RandomSource random)
        {
            return create(random, minSmall, maxSmall);
        }

        Gen.LongGen max(RandomSource random)
        {
            return create(random, minLarge, maxLarge);
        }

        private Gen.LongGen create(RandomSource random, int min, int max)
        {
            switch (type)
            {
                case IntRange: return new IntRange(min, max);
                case RandomWalkRange: return new RandomWalkRange(random, min, max);
                default: throw new UnsupportedOperationException("type " + type);
            }
        }

        double ratio()
        {
            return 1 / (double) largeRatio;
        }

        @Override
        public String toString()
        {
            return "TestCase{" +
                    "minSmall=" + minSmall +
                    ", maxSmall=" + maxSmall +
                    ", minLarge=" + minLarge +
                    ", maxLarge=" + maxLarge +
                    ", largeRatio=" + largeRatio +
                    ", type=" + type +
                    '}';
        }
    }

    @Test
    void disjoint()
    {
        Gen<Integer> range = Gens.ints().between(100, 100000000);
        Gen<Integer> ratio = Gens.ints().between(6, 20);
        Gen<Type> typeGen = Gens.enums().all(Type.class);
        int[] ints = new int[3];
        Gen<TestCase> test = rs -> {
            ints[0] = range.next(rs);
            ints[1] = range.next(rs);
            ints[2] = range.next(rs);
            Arrays.sort(ints);
            int largeRatio = ratio.next(rs);
            return new TestCase(0, ints[0], ints[1], ints[2], largeRatio, typeGen.next(rs));
        };
        qt().forAll(Gens.random(), test).check(FrequentLargeRangeTest::test);
    }

    private static void test(RandomSource rs, TestCase tc)
    {
        double ratio = tc.ratio();
        FrequentLargeRange period = new FrequentLargeRange(tc.min(rs), tc.max(rs), ratio, tc.largeRatio);
        int numSamples = 1000;
        int maxResamples = 1000;

        double target = ratio * 100.0D;
        double upperBounds = target * 1.2;
        double lowerBounds = target * .8;

        int largeSeq = 0;
        int largeCount = 0;
        Long2LongHashMap largeSeqCounts = new Long2LongHashMap(0);
        int resamples = 0;
        for (int i = 0; i < numSamples; i++)
        {
            long size = period.nextLong(rs);
            if (size > tc.maxSmall)
            {
                largeCount++;
                largeSeq++;
            }
            else
            {
                largeSeqCounts.compute(largeSeq, (ignore, accm) -> accm + 1);
                largeSeq = 0;
            }
            if (i == numSamples - 1)
            {
                // keep going if ratio is off...
                double actual = (double) largeCount / numSamples * 100.0;
                if (actual < lowerBounds || actual  > upperBounds)
                {
                    if (resamples == maxResamples)
                        throw new AssertionError(String.format("Unable to match target rate in %d re-samples; actual=%f, target=%f, bounds=(%f, %f)", resamples, actual, target, lowerBounds, upperBounds));
                    numSamples += 100;
                    resamples++;
                }
            }
        }

        checkSequences(largeSeqCounts);
        assertRatio(numSamples, upperBounds, lowerBounds, largeCount);
    }

    private static void checkSequences(Long2LongHashMap largeSeqCounts)
    {
        long[] keys = new long[largeSeqCounts.size()];
        Long2LongHashMap.KeyIterator it = largeSeqCounts.keySet().iterator();
        int idx = 0;
        while (it.hasNext())
            keys[idx++] = it.nextValue();
        if (LongStream.of(keys).anyMatch(seq -> seq > 5))
            return;
        StringBuilder sb = new StringBuilder("No large sequences detected; saw\n");
        Arrays.sort(keys);
        for (long key : keys)
            sb.append('\t').append(key).append('\t').append(largeSeqCounts.get(key)).append('\n');
        throw new AssertionError(sb.toString());
    }

    private static void assertRatio(int numSamples, double upperBounds, double lowerBounds, long largeCount)
    {
        double largePercent = largeCount / (double) numSamples * 100.0;
        Assertions.assertThat(largePercent)
                .describedAs("Expected ratio was not respected")
                .isBetween(lowerBounds, upperBounds);
    }
}