package com.flextrade.jfixture.builders;

import com.flextrade.jfixture.NoSpecimen;
import com.flextrade.jfixture.SpecimenBuilder;
import com.flextrade.jfixture.SpecimenContext;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;

public class NumberInRangeGenerator implements SpecimenBuilder {

    private final long[] limits;
    private final HashSet<Long> numbers;
    private long lower;
    private long upper;
    private long count;
    private final Random random;

    public NumberInRangeGenerator() {
        this(1, Byte.MAX_VALUE, Short.MAX_VALUE, Integer.MAX_VALUE);
    }

    public NumberInRangeGenerator(long... limits) {
        this.limits = limits;
        this.numbers = new HashSet<Long>();
        this.random = new Random();
        this.createRange();
    }

    @Override
    public Object create(Object request, SpecimenContext context) {
        if (request.equals(Byte.class)) {
            return (byte) getNextRandom();
        } else if (request.equals(Short.class)) {
            return (short) getNextRandom();
        } else if (request.equals(Integer.class)) {
            return (int) getNextRandom();
        } else if (request.equals(Long.class)) {
            return getNextRandom();
        } else if (request.equals(Float.class)) {
            return (float) getNextRandom();
        } else if (request.equals(Double.class)) {
            return (double) getNextRandom();
        } else if (request.equals(BigDecimal.class)) {
            return BigDecimal.valueOf(getNextRandom());
        } else if (request.equals(BigInteger.class)) {
            return BigInteger.valueOf(getNextRandom());
        }

        return new NoSpecimen();
    }

    private long getNextRandom() {
        this.evaluateRange();

        long result;
        do {
            result = (long)(this.random.nextDouble() * (this.upper - this.lower)) + this.lower;
        }
        while (this.numbers.contains(result));

        this.numbers.add(result);
        return result;
    }

    private void evaluateRange() {
        if (this.count == (this.upper - this.lower)) {
            this.count = 0;
            this.createRange();
        }

        this.count++;
    }

    private void createRange() {
        List<Long> remaining = getRemainingValues();
        if (remaining.size() > 0 && this.numbers.size() > 0) {
            this.lower = this.upper;
            this.upper = Collections.min(remaining) + 1;
        } else {
            this.lower = limits[0];
            this.upper = limits[1];
        }

        this.numbers.clear();
    }

    private List<Long> getRemainingValues() {
        List<Long> remaining = new ArrayList<Long>();
        for (long l : this.limits) {
            if (l > this.upper - 1) remaining.add(l);
        }

        return remaining;
    }
}